Skip to content

fix(security): 実行時フロアの回避経路 2 件を塞ぐ (Phase 128) - #282

Merged
Chachamaru127 merged 4 commits into
mainfrom
Chachamaru127/main
Jul 31, 2026
Merged

Chachamaru127 merged 4 commits into
mainfrom
Chachamaru127/main

Conversation

@Chachamaru127

@Chachamaru127 Chachamaru127 commented Jul 31, 2026 •

Copy link
Copy Markdown
Owner

外部からの PR #246 をレビューする過程で、現在の main に実在する実行時フロアの回避経路 2 件を発見しました。いずれも実際に動かして確認しています。#246 の採否とは独立に、単独で修正すべき最優先事項として切り出したものです。

修正はすべて制限を強める方向のみで、既存の拒否・確認ルールを緩めた箇所はありません。

回避経路 A — AI が自分で秘密ファイルの読み取り制限を外せた

3 段すべてが成立していました。

# 内容 確認方法
1 設定ファイルはどの拒否リストにもなく、AI が書ける 拒否 22 件を機械確認(該当 0 件)+ hook 実測で「判定なし = 通る」
2 secretAllow の相対パス宣言だけ境界検査がない ソース確認(絶対パス側のみ pathUnderWorktree)
3 相対パスは作業ツリーの外へ出る パス計算を実測: ../../../../etc/a → /etc/a

つまり AI が {"runtimefloor":{"secretAllow":["../../../../etc/shadow"]}} と自分で書けば、仕様書が「いかなる設定・環境変数・permission mode でも上書きできない」と定める秘密読み取りフロアを自力で無効化できました。

仕様書は元から正しかった点が重要です。spec.md は既に「相対パスはプロジェクトルート配下に解決される」と書いており、実装がそれを守っていませんでした。新しい制約の追加ではなく、仕様どおりへの是正です。

2 層で塞ぎました

境界検査だけでは宣言を作業ツリー内に丸めるだけで、「AI が自分で .env を宣言して読む」経路が残ります。設定ファイル自体を保護対象に載せることで、フロアを広げる操作に人間を戻しました。

deny を選んだ根拠は実測です。ask に降格すると self-audit の追跡対象から外れ、将来この保護が削除されても機械的に検知できません。deny なら指紋化され、劣化が検出されます。副作用として releaseAuto の切り替えは手動編集になります。

回避経路 B — main への強制 push が確認なしで通っていた

保護ブランチの判定が、強制 push 短縮形の先頭 + を取り除かずに照合していました。

実測(再ビルドした実行ファイル)

操作 修正前 修正後
git push origin +main 素通り 確認
git push origin +refs/heads/main 素通り 確認
git push origin main 確認 確認(不変)
git push origin +feature/x(保護対象外) 素通り 素通り(非退行)
設定ファイル .json への書き込み 通る 拒否
設定ファイル .yaml への書き込み 通る 拒否
雛形 ...config.example.json 通る 通る(誤爆なし)
README.md 通る 通る(非退行)

「緩める変更ゼロ」の機械的根拠

go/ 全体で削除は 4 行だけです。すべて、より厳しい版への置換です。

削除行 実態
return strings.Trim(token, "'\"") + も剥がす版に置換(厳しく)
out = append(out, filepath.Join(...)) 境界検査付きに置換(厳しく)
R02 のハッシュ 拒否パターン追加に伴う機械的更新
R03 のハッシュ 同上

拒否・確認ルールは 1 つも削除・弱体化していません。

Constitution ゲート

rules.go 系は human-only 領域のため、Phase 112.19 / 126.6 と同じ手続きで operator の明示採択を経ています(2026-07-31、対象 7702ca45)。差分統計・削除行の全列挙・実測表・検証結果を提示のうえ採択を得ました。

selfaudit.go への接触は Phase 126 ではありませんでした。変更は R02 / R03 のハッシュ 2 行のみで、これを更新しないと hook が「拒否面が変わった」として判定自体を拒否するため、機械的に必須です。

検証(フロア免除の環境変数を設定したまま実行)

検査 結果
go test ./... 全パス(gofmt / vet clean)
tests/validate-plugin.sh 131 合格 / 0 失敗
scripts/ci/check-consistency.sh 全 24 通過
binary / source drift OK(4 プラットフォーム再ビルド済み)
skill mirror in-sync

レビューで 1 度差し戻しています

128.1 の初版は許可リストに解決後の実体パスを保存していました。実測すると次の状態でした。

declared: secrets/link.token
allowlist patterns: [".../secrets/real.token"]     ← 宣言ではなく実体
宣言した link.token を読む   -> allowed=false      ← 退行
宣言していない real.token を読む -> allowed=true    ← 緩和

後者は本 Phase の絶対条件「緩める箇所ゼロ」に違反します。差し戻し、symlink の解決を許可・拒否の判定にのみ使う形へ是正しました。同じ probe で修正後は declared=true / undeclared=false を確認しています。

副産物として、macOS の /var → /private/var symlink により、比較の片側だけを解決すると既存の宣言がすべて誤って拒否される問題も見つかり、同時に修正しました。

非接触

.github/workflows/、.claude-plugin/settings*、.claude/settings*、deny-baseline.json、VERSION / plugin.json / harness.toml はいずれも変更していません。

🤖 Generated with Claude Code

https://claude.ai/code/session_01SkVve4dyhMqf9LMcQtsSyC

Summary by CodeRabbit

  • セキュリティ
    • 許可されたシークレット参照がプロジェクト外へ抜ける経路を遮断しました。
    • シンボリックリンク経由の不正な外部参照も拒否します。
    • ハーネス設定ファイルへの書き込みやリダイレクトを拒否します。
    • 強制 push や強制 reset による保護ブランチの回避を修正しました。
    • 保護対象外のブランチやサンプル設定ファイルは従来どおり利用できます。

tachibanashuuta and others added 4 commits July 30, 2026 12:50
外部 PR #246 のセキュリティレビュー中に、現在の main に実在する
フロア回避経路 2 件を発見。いずれも Lead が実測で確認済み。
#246 の採否とは独立に単独修正すべき最優先事項として起票する。

回避経路 A (P0): AI が自力で secret-read hard floor を外せる。
3 段すべて実測で成立:
 1. .claude-code-harness.config.json は deny リストに無く AI が書ける
    (deny 22 件中ヒット 0、hook 実測でも Write は「判定なし = 通る」)
 2. configSecretAllowPatterns は絶対パス宣言にだけ境界チェックがあり、
    相対パス宣言には無い
 3. パス計算実測: ../../../../etc/a -> /etc/a と作業ツリー外へ出る

回避経路 B (P1): main への強制 push が確認をすり抜ける。
hook 実測: `git push origin main` -> ask だが
`git push origin +main` / `+refs/heads/main` は判定なし (素通り)。
force refspec の先頭 + が正規表現照合前に剥がされていない。

A は 2 層で塞ぐ。境界チェック (128.1) だけでは宣言を worktree 内に
丸めるのみで「AI が自分で .env を宣言して読む」経路が残るため、
設定ファイル自体を保護面に載せる (128.3)。128.3 は helpers.go を
触るため Constitution 条項に該当し、operator adoption gate (128.4)
を置く (Phase 112.19 / 126.6 の前例に従う)。

対象外は Plans.md に明示 (dependabot 9 件 / #246 の R16 再実装 /
allow_rm_rf)。黙って落とさず理由付きで Reject 分類。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
外部 PR #246 のレビュー中に発見した、現在の main に実在する
runtime floor 回避経路 2 件を修正する。変更はすべて締める方向のみ。

## 128.1 — secretAllow の相対パス宣言が worktree を脱出できた

configSecretAllowPatterns は絶対パス宣言にだけ pathUnderWorktree を
適用しており、相対パス宣言は filepath.Join した結果を無検証で許可
リストに載せていた。`../../../../etc/shadow` のような宣言で
secret-read hard floor を設定ファイルから無効化できた。

spec.md は元から "Relative paths resolve under the project root" と
書いており、これは新しい制約の追加ではなく仕様どおりへの是正である。

両分岐を解決後の絶対パスで境界判定するよう統一し、さらに
secretAllowSymlinkStaysInWorktree を追加して「テキスト上は worktree 内
だが symlink で外を指す」宣言も落とす。symlink 解決は pass/deny の
判定にのみ使い、許可リストには宣言由来のパスを保存する
(実体パスを保存すると、宣言したパスが自分の宣言に一致しなくなる退行と、
宣言していない実体パスが許可される緩和が同時に起きる。Lead の probe で
両方を実測し差し戻した)。

## 128.2 — main への強制 push が確認をすり抜けた

protectedBranchRefPattern が force refspec 先頭の "+" を剥がさないため、
`git push origin +main` が R12 の ask をすり抜けていた。
normalizeGitToken で "+" を除去し、R11 (reset --hard) にも同時に効かせる。

## 128.3 — 設定ファイルを AI が書き換えられた

.claude-code-harness.config.{json,yaml} は runtimefloor.secretAllow を
通じて hard floor の適用範囲を決めるが、どの deny リストにも無く
AI が自由に書けた。128.1 の境界チェックだけでは宣言を worktree 内に
丸めるのみで「AI が自分で宣言して読む」経路が残る。
protectedPathRules に deny レベルで追加し、selfaudit の deny surface
baseline (R02/R03) を機械的に更新した。

ask ではなく deny を選んだ根拠: ask レベルのパターンは selfaudit の
deny surface に含まれないため、将来この保護が削除されても機械的に
検知できない。deny なら指紋化され、劣化が検出される (Worker が両案を
実測して発見)。

## 検証 (floor 免除 env を export したまま実行)

hook 実測 (再ビルドした binary):
  git push origin +main                    素通り -> ask
  git push origin +refs/heads/main         素通り -> ask
  git push origin +feature/x (保護外)       素通り -> 素通り
  .claude-code-harness.config.json 書込     通る -> deny
  .claude-code-harness.config.yaml 書込     通る -> deny
  claude-code-harness.config.example.json  通る -> 通る
  README.md                                通る -> 通る

- go test ./... 全パス / gofmt + vet clean
- tests/validate-plugin.sh 131 pass / 0 fail
- scripts/ci/check-consistency.sh 全 24 通過
- check-binary-source-drift.sh OK (4-platform 再ビルド済み)
- 削除行は go/ 全体で 4 行のみ。内訳は (1) token 正規化の置換
  (より厳しく)、(2)(3) deny 追加に伴う baseline hash 更新、
  (4) 境界チェック付きへの置換 (より厳しく)。
  deny/ask ルールの削除・弱体化はゼロ

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
3 task すべて 7702ca4 で実装完了。128.1 は Lead レビューで 1 度差し戻し
(許可リストに実体パスを保存していたため退行と緩和が同時発生) し、
probe 実測で修正を確認してから統合した。

残 3 件: 128.4 (operator 採択ゲート) / 128.5 (PR closeout) / 128.6 (#246 closeout)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CHANGELOG [Unreleased] に Security 節を新設し、2 つの回避経路を
before/after で記述した。実測表 (hook 判定の修正前後) も併記。

128.4 (operator 採択ゲート) を cc:done に更新。
2026-07-31 に operator が 7702ca4 を明示採択。
提示材料: 差分統計 / 削除行 4 行の全列挙 / hook 実測表 / 検証結果。

検証:
- VERSION / plugin.json / harness.toml は未変更
- skill mirror in-sync (codex / opencode)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 31, 2026 •

Copy link
Copy Markdown

Review Change Stack

Walkthrough

runtimefloor.secretAllow の作業ツリー境界と symlink を検証し、ハーネス設定ファイルを保護対象へ追加しました。さらに、強制 refspec の先頭 + を正規化し、保護ブランチへの reset・push 判定を修正しました。

Changes

セキュリティ保護強化

Layer / File(s) Summary
secretAllow の境界検証
go/internal/runtimefloor/runtimefloor.go, go/internal/runtimefloor/runtimefloor_test.go, spec.md, CHANGELOG.md, Plans.md
secretAllow の相対・絶対パスを解決後の作業ツリー境界で検証し、外部 symlink を拒否します。内部 symlink、未作成の内部パス、宣言パス保持の挙動を仕様・テストで確認します。
保護パスと強制 refspec のポリシー
go/internal/policy/helpers.go, go/internal/policy/rules_test.go, go/internal/policy/selfaudit.go, spec.md, CHANGELOG.md, Plans.md
.claude-code-harness.config.json、.yaml、.yml を拒否対象に追加し、refspec の先頭 + を除去して保護ブランチ判定へ渡します。設定ファイル操作、reset、force push、非保護ブランチのテストと deny-surface 署名を更新します。

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Poem

ぴょんと跳ね、境界線を見張る
symlink の道も外へは行かせない
+main も見逃さず
設定の扉を固く守る
にんじん色の安全強化、完成!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed タイトルは、実行時フロアに関する2件のセキュリティ回避経路を修正するという変更の主目的を簡潔かつ具体的に表しています。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch Chachamaru127/main

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain modules listed in go.work or their selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Chachamaru127
Chachamaru127 merged commit 1f085a3 into main Jul 31, 2026
8 of 9 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 476ea40304

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// floor by editing the declaration that scopes it. Does not match the
// checked-in template "claude-code-harness.config.example.json" (no leading
// dot, distinct ".example." infix).
{protectedPathDeny, "harness control-plane config", regexp.MustCompile(`(?:^|/)\.claude-code-harness\.config\.(?:json|ya?ml)$`)},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Cover non-redirection writes to the control-plane config

Protecting this pathname does not prevent an AI from replacing it with commands such as cp /tmp/config .claude-code-harness.config.json or mv ...: R03 obtains destinations exclusively from shell redirections and tee in extractBashWriteTargets, so these common Bash writes return no protected target and are approved. The replacement can then enable runtimefloor.releaseAuto or expand secretAllow, leaving the self-modification route this change is intended to close.

Useful? React with 👍 / 👎.

Comment on lines +414 to +415
if err != nil {
return true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Resolve existing parent symlinks before accepting missing paths

Returning true for every EvalSymlinks error reopens the symlink escape for not-yet-created declarations. If config allows link/.env while link is absent, a single preflighted command such as ln -s /outside link && cat link/.env is approved because the initial resolution fails and the lexical path is allowlisted; execution then creates the escaping symlink before reading the outside file. Resolve the longest existing parent and append the missing suffix, as the policy package already does, rather than treating ENOENT as proof that the declaration stays inside the worktree.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@go/internal/policy/helpers.go`:
- Around line 73-80: Update the protectedPathDeny regexp for
.claude-code-harness.config files to use case-insensitive matching via an inline
(?i:...) group, preventing casing variants from bypassing R02/R03. Add
regression tests covering uppercase or mixed-case JSON/YAML/YML variants and
verify they are denied.

In `@go/internal/runtimefloor/runtimefloor.go`:
- Around line 391-419: Update secretAllowSymlinkStaysInWorktree and
isAllowlistedSecretPath so declared-but-nonexistent paths cannot bypass worktree
symlink checks after being replaced. Resolve and validate the nearest existing
ancestor when EvalSymlinks fails, or revalidate the declared path with the same
token during secret reads, while preserving allowlisting only for paths
confirmed to remain inside the worktree.

In `@Plans.md`:
- Around line 167-168: Update the 128.5 entry in Plans.md to reflect the actual
closeout status and results: mark its Status/result as complete only if the
listed validation, drift, consistency, version-surface, and PR closeout DoD
items are finished. If any remain incomplete, revise the PR objectives and
CHANGELOG wording to clearly identify the remaining work instead of reporting
completion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 04fc5ac5-5191-473a-a9ac-8febd34b5ad0

📥 Commits

Reviewing files that changed from the base of the PR and between e2e87f3 and 476ea40.

⛔ Files ignored due to path filters (1)
  • bin/harness-windows-amd64.exe is excluded by !**/*.exe
📒 Files selected for processing (11)
  • CHANGELOG.md
  • Plans.md
  • bin/harness-darwin-amd64
  • bin/harness-darwin-arm64
  • bin/harness-linux-amd64
  • go/internal/policy/helpers.go
  • go/internal/policy/rules_test.go
  • go/internal/policy/selfaudit.go
  • go/internal/runtimefloor/runtimefloor.go
  • go/internal/runtimefloor/runtimefloor_test.go
  • spec.md
👮 Files not reviewed due to content moderation or server errors (1)
  • bin/harness-darwin-arm64

Comment on lines +73 to +80
// Phase 128.3: .claude-code-harness.config.{json,yaml,yml} governs
// runtimefloor.secretAllow / runtimefloor.releaseAuto and other control-plane
// settings, the same governance tier as .claude/settings* and
// .claude-plugin/settings*. Denied so the AI cannot loosen its own hard
// floor by editing the declaration that scopes it. Does not match the
// checked-in template "claude-code-harness.config.example.json" (no leading
// dot, distinct ".example." infix).
{protectedPathDeny, "harness control-plane config", regexp.MustCompile(`(?:^|/)\.claude-code-harness\.config\.(?:json|ya?ml)$`)},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- candidate file outline ---'
ast-grep outline go/internal/policy/helpers.go --view compact || true
printf '%s\n' '--- protected path rules and nearby logic ---'
cat -n go/internal/policy/helpers.go | sed -n '1,110p'
printf '%s\n' '--- relevant tests and references ---'
rg -n -i 'R02|R03|protectedPathRules|claude-code-harness\.config|protectedPathDeny' go

Repository: Chachamaru127/claude-code-harness

Length of output: 6575


🏁 Script executed:

#!/bin/bash
set -u
printf '%s\n' '--- helper structure ---'
ast-grep outline go/internal/policy/helpers.go --view signatures || true
printf '%s\n' '--- remaining helper implementation ---'
cat -n go/internal/policy/helpers.go | sed -n '95,180p'
printf '%s\n' '--- policy tests and exact references ---'
rg -n -i 'R02|R03|classifyProtectedPathPattern|normalizePathForGuardrail|protectedPath' go/internal go/pkg || true
printf '%s\n' '--- filesystem/path policy context ---'
rg -n -i 'filepath\.|os\.|runtime\.GOOS|GOOS|case.?insens|windows|darwin|protected path|guardrail' go/internal/policy go/internal go/pkg || true

Repository: Chachamaru127/claude-code-harness

Length of output: 7929


🏁 Script executed:

#!/bin/bash
set -u
printf '%s\n' '--- repository test files ---'
git ls-files | rg -i '(^|/)([^/]*(test|spec)[^/]*|tests?/.*)$' | head -200
printf '%s\n' '--- all R02/R03 references ---'
rg -n -i 'R0[23]|r02|r03' . --glob '!vendor/**' --glob '!node_modules/**' || true
printf '%s\n' '--- protected-path call sites ---'
rg -n 'classifyProtectedPath|classifyProtectedPathAtRoot|isProtectedPath|protectedPathRules' go --glob '*.go' || true
printf '%s\n' '--- standalone regexp behavior probe ---'
python3 - <<'PY'
import re
pattern = re.compile(r'(?:^|/)\.claude-code-harness\.config\.(?:json|ya?ml)$')
for path in [
    '.claude-code-harness.config.json',
    '.CLAUDE-CODE-HARNESS.CONFIG.JSON',
    'dir/.CLAUDE-CODE-HARNESS.CONFIG.YAML',
    'claude-code-harness.config.example.json',
]:
    print(f'{path}: {bool(pattern.search(path))} / ignore-case: {bool(re.compile(pattern.pattern, re.I).search(path))}')
PY

Repository: Chachamaru127/claude-code-harness

Length of output: 9342


🏁 Script executed:

#!/bin/bash
set -u
printf '%s\n' '--- policy helper tests ---'
cat -n go/internal/policy/helpers_test.go | sed -n '1,260p'
printf '%s\n' '--- policy regression tests ---'
cat -n go/internal/policy/cc2110_regression_test.go | sed -n '1,260p'
printf '%s\n' '--- config-specific tests ---'
rg -n -i -C 5 'harness.?config|control.?plane|runtimefloor|releaseAuto|R02|R03' go/internal/policy --glob '*_test.go' || true
printf '%s\n' '--- exact symbol references (fixed search) ---'
grep -RInE 'classifyProtectedPath|protectedPathRules|protectedPathDeny' go/internal/policy --include='*.go' || true

Repository: Chachamaru127/claude-code-harness

Length of output: 50389


🏁 Script executed:

#!/bin/bash
set -u
printf '%s\n' '--- R02/R03 implementation around protected paths ---'
rg -n -C 12 'classifyProtectedPathAtRoot|classifyProtectedPath\(|R02|R03' go/internal/policy/rules.go go/internal/policy/*.go | head -240
printf '%s\n' '--- existing harness-config tests only ---'
rg -n -C 4 'HarnessConfig|claude-code-harness\.config' go/internal/policy --glob '*_test.go'
printf '%s\n' '--- case-variant coverage in policy tests ---'
rg -n -i 'CLAUDE-CODE-HARNESS|CONFIG\.(JSON|YAML|YML)' go/internal/policy --glob '*_test.go' || true
printf '%s\n' '--- path normalization platform behavior ---'
rg -n -C 5 'normalizePathForGuardrail|filepath\.ToSlash|filepath\.Clean|EvalSymlinks' go/internal/policy/helpers.go

Repository: Chachamaru127/claude-code-harness

Length of output: 30708


🏁 Script executed:

#!/bin/bash
set -u
cat -n go/internal/policy/helpers.go | sed -n '245,298p'

Repository: Chachamaru127/claude-code-harness

Length of output: 2127


設定ファイルの大小文字違いによる deny 回避を防止してください。

regexp は大小文字を区別するため、ケース非依存ファイルシステムでは .CLAUDE-CODE-HARNESS.CONFIG.JSON が R02/R03 を回避します。パターンを (?i:...) で照合し、大小文字違いの R02/R03 回帰テストを追加してください。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/internal/policy/helpers.go` around lines 73 - 80, Update the
protectedPathDeny regexp for .claude-code-harness.config files to use
case-insensitive matching via an inline (?i:...) group, preventing casing
variants from bypassing R02/R03. Add regression tests covering uppercase or
mixed-case JSON/YAML/YML variants and verify they are denied.

Comment on lines +391 to +419
// secretAllowSymlinkStaysInWorktree reports whether a worktree-bound
// secretAllow declaration stays inside the worktree after symlinks are
// resolved. rootResolved must already be EvalSymlinks()-resolved (or its
// EvalSymlinks fallback) so both sides of the comparison are in the same
// coordinate space — see the comment in configSecretAllowPatterns about the
// macOS /var -> /private/var symlink. This closes an escape where a
// declaration names a path that is textually inside the worktree but is (or
// contains) a symlink pointing outside it (same class of escape as Phase
// 126.4's R05 use of EvalSymlinks). Symlink resolution is used ONLY to
// decide pass/deny here — the allowlist itself must keep storing the
// operator's DECLARED path (the caller appends its own `abs`/`joined` value,
// not this function's return), because isAllowlistedSecretPath() matches the
// token the command names against declared patterns via strings.HasPrefix.
// Substituting the resolved realpath into the allowlist would both (a) stop
// matching the operator's own declared path (regression) and (b) start
// matching the realpath even though the operator never declared it
// (widening) — the declaration and the allowlist entry must refer to the
// same path. If the symlink cannot be resolved (path does not exist yet,
// permission error, etc.) this reports true — fail-safe here means "do not
// widen the allowlist" for a resolved escape, not "silently drop a
// legitimate not-yet-created declaration".
func secretAllowSymlinkStaysInWorktree(path, rootResolved string) bool {
resolved, err := filepath.EvalSymlinks(path)
if err != nil {
return true
}
return pathUnderWorktree(filepath.Clean(resolved), rootResolved)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: isAllowlistedSecretPath の実装とその呼び出し箇所(実際の読み取り時の再検証有無)を確認
rg -n -C 8 'isAllowlistedSecretPath|secretAllowSymlinkStaysInWorktree' go/internal/runtimefloor

Repository: Chachamaru127/claude-code-harness

Length of output: 171


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)runtimefloor\.go$|go/internal/runtimefloor|secretAllow|EvalSymlinks'

echo "== runtimefloor outline/stat =="
f=$(git ls-files | grep 'go/internal/runtimefloor/runtimefloor.go' | head -n 1)
if [ -n "${f:-}" ]; then
  wc -l "$f"
  ast-grep outline "$f" --view compact | sed -n '1,220p' || true
  echo "== targeted search =="
  rg -n -C 5 'secretAllowSymlinkStaysInWorktree|isAllowlistedSecretPath|EvalSymlinks|pathUnderWorktree|secretAllow' "$f" || true
  echo "== surrounding lines =="
  sed -n '360,440p' "$f" | cat -n
fi

Repository: Chachamaru127/claude-code-harness

Length of output: 11008


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant implementation sections =="
sed -n '450,510p' go/internal/runtimefloor/runtimefloor.go | cat -n
sed -n '563,590p' go/internal/runtimefloor/runtimefloor.go | cat -n

echo "== all secretRead-related locations =="
rg -n -C 4 'CategorySecretRead|secretAllow|hasSecretRead|secret|credential|EvalSymlinks' go/internal/runtimefloor/runtimefloor.go go/internal | sed -n '1,240p'

echo "== behavioral probe: filepath.EvalSymlinks vs declared allowlist matching =="
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
root("$tmpdir"/worktree)
rgx='strings\HasPrefix\("declared/path", patterns\)'
cat > /tmp/go_evalprobe.go <<'GO'
package main

import (
	"fmt"
	"path/filepath"
)

func main() {
	tmp := "/tmp/go-floortmp123"
	path := filepath.Join(tmp, "declared/path.log")
	pattern := filepath.Join(tmp, "declared/path")
	resolved, err := filepath.EvalSymlinks(path)
	fmt.Printf("path=%q\npattern=%q\nerr=%v\nresolved=%q\n", path, pattern, err, resolved)
	if err == nil {
		fmt.Printf("prefix_match_after_resolve=%v\n", resolved == pattern || filepath.HasPrefix(resolved, pattern+string(filepath.Separator)))
	}
	fmt.Printf("prefix_match_on_declared=%v\n", path == pattern || filepath.HasPrefix(path, pattern+string(filepath.Separator)))
}
GO
if command -v go >/dev/null 2>&1; then
  go run /tmp/go_evalprobe.go
fi

Repository: Chachamaru127/claude-code-harness

Length of output: 16048


未存在宣言 path は fail-open を戻す必要がある

secretAllowSymlinkStaysInWorktree() は EvalSymlinks() がエラーだと true を返し、許可リストには宣言された未存在パスそのままで入る仕様です。その後で宣言パスにワークツリー外を指す symlink が差し替われば、isAllowlistedSecretPath() は宣言トークンと宣言パターンで一致するため、秘密読み取りでも worktree-escape が再導入されます。存在する直近の祖先 symlink をresolveして検査するか、秘密読み取り時に宣言パスと同じtokenで再検証してください。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/internal/runtimefloor/runtimefloor.go` around lines 391 - 419, Update
secretAllowSymlinkStaysInWorktree and isAllowlistedSecretPath so
declared-but-nonexistent paths cannot bypass worktree symlink checks after being
replaced. Resolve and validate the nearest existing ancestor when EvalSymlinks
fails, or revalidate the declared path with the same token during secret reads,
while preserving allowlisting only for paths confirmed to remain inside the
worktree.

Comment thread Plans.md
Comment on lines +167 to +168
| 128.5 | `[lane:release]` `[tdd:skip:verification]` 検証 + 配布物 + closeout: `go/scripts/build-all.sh` で 4-platform binary 再生成 (`bin/harness` は shim のため `-o bin/harness` 禁止)、`scripts/ci/check-binary-source-drift.sh` ローカル green、新 test を `tests/validate-plugin.sh` へ配線、CHANGELOG `[Unreleased]` に Security 節として追記 (回避経路と塞ぎ方を before/after で) | (a) `bash tests/validate-plugin.sh` 0 failed (floor 免除 env を export したまま), (b) drift gate green, (c) `bash scripts/ci/check-consistency.sh` 全パス, (d) VERSION / `.claude-plugin/plugin.json` / `harness.toml` 非接触を `git diff --name-only` で確認, (e) PR closeout (事前承認済み: push + PR 作成 + CI green 確認 + merge。merge 前に 128.4 の採択を確認) | 128.4 | cc:TODO |
| 128.6 | `[lane:fast]` `[tdd:skip:no-code-change]` PR #246 の closeout: レビュー結論 (critical 3 件で as-is マージ不可、ただし問題提起は正当で脆弱性 2 件の発見に繋がった) を作者向けに説明してクローズする。採用した部分 (本 Phase の 2 件) と、再実装に回した部分 (`paths.protected` / R16、`git.protected_branches` 加算)、見送った部分 (`allow_rm_rf`) を切り分けて伝える。rebase 依頼はしない (117 commit 差 + Phase 126 が R05/R12 のコア実装を書き換えており、こちらで再実装する方が速く安全) | (a) #246 に日本語でなく英語のコメント (作者は英語話者) で 3 分類を明示, (b) 本 Phase の 2 件が #246 由来であることを明記して credit する, (c) #246 が CLOSED になる | 128.5 | cc:TODO |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n '128\.5|cc:TODO|validate-plugin|check-consistency|binary-source-drift' Plans.md CHANGELOG.md

Repository: Chachamaru127/claude-code-harness

Length of output: 20981


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the exact Phase 128 table row and nearby context.
sed -n '150,180p' Plans.md | nl -ba -v133

# Show whether Plans.md has any explicit 128.5 closeout/result area or if Status is the only source.
python3 - <<'PY'
from pathlib import Path
text = Path('Plans.md').read_text()
for needle in ['Phase 128', '128.5', '128.4', '128.6', 'cc:TODO']:
    print(needle, text.count(needle))
PY

Repository: Chachamaru127/claude-code-harness

Length of output: 211


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '150,180p' Plans.md

python3 - <<'PY'
from pathlib import Path
text = Path('Plans.md').read_text().splitlines()
for i, line in enumerate(text[149:178], start=150):
    if '128.5' in line:
        cells = [c.strip().rstrip('|') for c in line.split('|')]
        print(f'row: {i}')
        print('last_two_nonempty_cells:', [c for c in cells if c][-2:])
PY

Repository: Chachamaru127/claude-code-harness

Length of output: 8690


128.5 の Status を完了報告に合わせて更新してください。

DoD は検証・drift gate・整合性チェック・Version surface 不接触・PR closeout の完了を求めています。もし本 Phase の closeout が終了しているなら、128.5 | ... | 128.4 | cc:TODO を最新 Status/result に更新してください。未完了なら PR の範囲説明と一致させるため、PR objectives/CHANGELOG の表現を未完成タスクとして明確にしてください。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Plans.md` around lines 167 - 168, Update the 128.5 entry in Plans.md to
reflect the actual closeout status and results: mark its Status/result as
complete only if the listed validation, drift, consistency, version-surface, and
PR closeout DoD items are finished. If any remain incomplete, revise the PR
objectives and CHANGELOG wording to clearly identify the remaining work instead
of reporting completion.

Chachamaru127 added a commit that referenced this pull request Jul 31, 2026
PR #282 merged to main (1f085a3)。必須 CI 8 件すべて pass。
#246 は 3 分類を明示した英語コメント付きでクローズ。

merge 後の main 上で最終実測: `+main` → ask /
`+feature/x` → 素通り (非退行) / 設定ファイル書込 → deny。

Co-authored-by: tachibanashuuta <tachibana@canai.jp>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
pull Bot pushed a commit to dubbypanda/claude-code-harness that referenced this pull request Aug 3, 2026
PR Chachamaru127#282 は squash merge されたため、Plans.md Phase 128 (128.1-128.5) が
記録していた作業ブランチ側の commit hash (7702ca4 / 476ea40) は main の
祖先から消えていた (git merge-base --is-ancestor で実測確認)。main に実際に
入った実体は 1f085a3 (親 1 個の squash commit、git log -1 --format=%p で
確認)。台帳を 1f085a3 に訂正し、なぜ変わったかの注記を Phase 128 見出し直下
に追加した。table の列数は編集前後で完全一致することを awk で確認済み。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZBxNEtYJbtHkZcsAn8nsv
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant