From b211ad67685cca3a844eeb323745f60e14b7edee Mon Sep 17 00:00:00 2001 From: Yuichi TSUNEMATSU Date: Thu, 3 Sep 2026 10:14:51 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat(claude):=20=E3=83=AB=E3=83=BC=E3=83=97?= =?UTF-8?q?=E3=83=90=E3=83=83=E3=82=AF=E3=82=92=20sandbox=20=E5=86=85?= =?UTF-8?q?=E3=81=A7=E8=A8=B1=E5=8F=AF=E3=81=97=E6=89=BF=E8=AA=8D=E3=83=97?= =?UTF-8?q?=E3=83=AD=E3=83=B3=E3=83=97=E3=83=88=E3=82=92=E5=89=8A=E6=B8=9B?= =?UTF-8?q?=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 直近8日の全セッション(Bash 呼び出し 10,635 件)を集計したところ、承認 プロンプトは週約 2,400 回発生していた。発生源は3つで、いずれも許可リスト の不足ではない。`autoAllowBashIfSandboxed` があるためサンドボックス内で 完結するコマンドは既に自動許可されており、`npm run test` などを allow に 足しても何も変わらない。 最大の発生源はサンドボックス外実行(週 1,941 回)で、その 47%(926 回)が ローカル TCP の遮断によるものだった。サンドボックス内で実測すると `connect 127.0.0.1:5432` と `bind/listen 127.0.0.1:0` が両方とも EPERM で 落ちる。統合テスト、E2E、dev サーバ、Playwright、Go の httptest、 `curl http://localhost:*` がこれで失敗し、そのたびに昇格が必要になり、 bash-guard.sh が承認を求めていた。 `sandbox.network.allowLocalBinding` を有効にすると seatbelt プロファイルに network-bind / network-inbound / localhost への network-outbound が出る。 適用後に同じ測定を行い、両方とも成功することを確認した。外部ネットワーク の制限は変わらない。 第二の発生源は docker で、`excludedCommands` により常にサンドボックス外で 走るのに allow ルールが無く、週約 440 回プロンプトを出していた。読み取り 専用のサブコマンドだけを allow に加える。`docker exec` と `compose exec` はコンテナ内で任意コマンドを実行できるため承認を維持する。 第三に、週3回以上使っている MCP の読み取り系ツール3件を allow に加える。 あわせて、承認ダイアログに出るのが description だけであるのに、ヒア ドキュメントで渡す長いスクリプトの説明が無く可否を判断できない問題を直す。 bash-guard.sh に、承認を要する経路(escape と docker の変更系)で description が 80 バイト未満なら deny を返す門を追加した。ask ではなく deny なのは、deny の理由だけがモデルに返るため、説明を書き直させられる からである。ask にするとユーザに丸投げになる。サンドボックス内で完結する コマンドは対象外なので、通常の作業に摩擦は増えない。 見込みは週約 2,400 回から約 1,100 回。 Co-Authored-By: Claude Opus 5 (1M context) --- dot_claude/CLAUDE.md | 8 ++++++++ dot_claude/hooks/bash-guard.sh | 23 ++++++++++++++++++++++- dot_claude/settings.json | 18 ++++++++++++++++-- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/dot_claude/CLAUDE.md b/dot_claude/CLAUDE.md index a44a700..1817da3 100644 --- a/dot_claude/CLAUDE.md +++ b/dot_claude/CLAUDE.md @@ -32,6 +32,14 @@ - 一時ファイルは `/tmp` 直下ではなくスクラッチパッド(`$TMPDIR`)に置くこと。`/tmp` はサンドボックスが書き込みを拒否する - `git` と `gh` はサンドボックス外で走る扱いのため、ビルドやテストと同じコマンドに混ぜないこと。`go test ./... && git commit ...` のような複合はコマンド全体が昇格対象になり、`gh` をループやパイプの中で呼ぶと自分の設定ファイルを読めずに失敗する - 破壊的な git 操作(force push、`reset --hard`、`clean -f`、履歴改変)は `bash-guard.sh` フックが拒否する。回避せず、必要なら人手での実行を提案すること +- ループバック接続(`localhost` / `127.0.0.1`)とローカルの listen は `sandbox.network.allowLocalBinding` で許可済み。統合テスト、E2E、dev サーバ、Playwright、Go の `httptest`、`curl http://localhost:*` はサンドボックス内で走るので escape を付けないこと +- `git` `gh` `docker` などは `sandbox.excludedCommands` により既にサンドボックス外で走る。これらに `dangerouslyDisableSandbox` を付けても実行結果は変わらず、承認プロンプトだけが増えるので付けないこと +- 承認プロンプトを伴うコマンド(`dangerouslyDisableSandbox` 付き、`docker exec` / `docker run` / `docker compose exec` などの変更系)では、`description` に日本語で次の4点を書くこと。ユーザはこの説明だけを見て可否を判断する + - 目的:何のために実行するのか + - 副作用:書き込むパス、触る DB とテーブル、接続先ホスト、削除や上書きの有無 + - 可逆性:元に戻せるか。戻し方、またはバックアップの場所 + - escape の理由:`dangerouslyDisableSandbox` を付ける場合、サンドボックス内で何がどう失敗したか + ヒアドキュメントで渡すスクリプトは、コマンド文字列を読ませて理解させるのではなく、`description` 側で何をするスクリプトかを説明すること ## 文章執筆 diff --git a/dot_claude/hooks/bash-guard.sh b/dot_claude/hooks/bash-guard.sh index cdf77cf..e913911 100644 --- a/dot_claude/hooks/bash-guard.sh +++ b/dot_claude/hooks/bash-guard.sh @@ -29,6 +29,7 @@ input=$(cat) cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // empty') || emit ask "フック入力の解析に失敗しました" escape=$(printf '%s' "$input" | jq -r '.tool_input.dangerouslyDisableSandbox // false') +desc=$(printf '%s' "$input" | jq -r '.tool_input.description // ""') # ヒアドキュメントの本文は実行されるコマンドではないので検査対象から外す。 # 含めたままにすると、コミットメッセージやドキュメントに書いた @@ -79,9 +80,29 @@ if git_sub filter-repo || git_sub filter-branch; then fi gh_sub 'repo[[:space:]]+delete' && emit deny "リポジトリの削除は禁止です" +# ---- deny: 承認を求める前に説明を書かせる ---- +# 承認プロンプトに出るのは description だけで、ヒアドキュメントで渡す長い +# スクリプトはユーザが読んで判断できる形になっていない。説明が薄いまま承認を +# 求めるのを止める。ask ではなく deny なのは、deny の理由だけがモデルに返り、 +# 説明を書き直して再提示させられるため。ユーザに丸投げしない。 +# +# サンドボックス内で完結するコマンドは自動許可されプロンプトが出ないので、 +# ここは実際に承認を要する経路(escape と docker の変更系)だけを対象にする。 +needs_detail=false +[ "$escape" = "true" ] && needs_detail=true +has '(^|[^[:alnum:]_-])docker[[:space:]]+(exec|run|cp|rm|rmi|build|buildx|volume|network|push|kill|stop)([^[:alnum:]_-]|$)' && + needs_detail=true +has '(^|[^[:alnum:]_-])docker[[:space:]]+compose[[:space:]]+([a-z-]+[[:space:]]+)*(up|down|exec|run|restart|rm|kill|stop)([^[:alnum:]_-]|$)' && + needs_detail=true + +# 80 バイト=日本語で約 27 文字。目的・副作用・可逆性を書けばまず超える。 +if [ "$needs_detail" = true ] && [ "${#desc}" -lt 80 ]; then + emit deny "承認が必要なコマンドですが description が短すぎて可否を判断できません(${#desc} バイト)。日本語で「目的/副作用(書き込むパス・触る DB とテーブル・接続先ホスト・削除や上書きの有無)/元に戻せるか/サンドボックス外で実行する理由」を書いた description を付けて再提示してください。ヒアドキュメントの中身も description 側で説明してください" +fi + # ---- ask: サンドボックス外実行 ---- [ "$escape" = "true" ] && - emit ask "サンドボックス外実行には承認が必要です。一時ファイルは /tmp ではなく \$TMPDIR を使い、git/gh はビルドやテストと同じコマンドに混ぜないでください" + emit ask "サンドボックス外実行には承認が必要です(説明: ${desc})。ループバック接続とローカル listen は allowLocalBinding で許可済みなので、それが理由なら escape は不要です" # ---- ask: 認証情報・書き込み系 API ---- gh_sub 'auth[[:space:]]+token' && emit ask "認証トークンの取り出しには承認が必要です" diff --git a/dot_claude/settings.json b/dot_claude/settings.json index 4fe95e2..e6b2743 100644 --- a/dot_claude/settings.json +++ b/dot_claude/settings.json @@ -42,7 +42,20 @@ "WebFetch(domain:registry.terraform.io)", "WebFetch(domain:docs.anthropic.com)", "WebFetch(domain:code.claude.com)", - "WebSearch(*)" + "WebSearch(*)", + "Bash(docker ps)", + "Bash(docker ps *)", + "Bash(docker images)", + "Bash(docker images *)", + "Bash(docker logs *)", + "Bash(docker inspect *)", + "Bash(docker version *)", + "Bash(docker compose ps)", + "Bash(docker compose ps *)", + "Bash(docker compose logs *)", + "mcp__claude_ai_Google_Drive__search_files", + "mcp__claude_ai_Notion__notion-fetch", + "mcp__claude_ai_Atlassian_Rovo__getJiraIssue" ], "ask": [ "Bash(terraform apply*)", @@ -191,7 +204,8 @@ "*.anthropic.com", "claude.ai", "*.claude.ai" - ] + ], + "allowLocalBinding": true } }, "statusLine": { From d6415d384ccbac5e2296766616560e83fbf77f71 Mon Sep 17 00:00:00 2001 From: Yuichi TSUNEMATSU Date: Thu, 3 Sep 2026 10:21:38 +0900 Subject: [PATCH 2/3] =?UTF-8?q?test(claude):=20bash-guard=20=E3=81=AE?= =?UTF-8?q?=E8=AA=AC=E6=98=8E=E5=BF=85=E9=A0=88=E3=82=B2=E3=83=BC=E3=83=88?= =?UTF-8?q?=E3=82=92=E6=A4=9C=E6=9F=BB=E3=81=AB=E8=BF=BD=E5=8A=A0=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit escape を ask とだけ期待していた検査が、説明必須ゲートの追加で落ちていた。 仕様が変わったのは意図どおりなので、検査側を追随させる。 `assert_decision` に description を渡せるようにし、既存の「サンドボックス外」 の検査には十分な長さの説明を与えて ask のままにした。そのうえで、新しい門の 振る舞いを4つの観点で固定する。 - 承認を要する経路(escape、docker の変更系)で説明が無い・短いと deny になる - 読み取り専用の docker とサンドボックス内のコマンドは説明を求めない。ここを 縛ると通常の作業に摩擦だけが増えるため、素通りであることを明示的に固定する - 閾値 80 バイトの境界。79 で deny、80 で ask - 破壊的操作の deny は説明の有無より優先する。説明を書けば force push が 通ってしまう、という抜け道が無いことを示す あわせて `docker-compose.yml` を含む grep が docker の変更系として誤検知 されないことも固定した。 pass=36 から pass=55 になる。 Co-Authored-By: Claude Opus 5 (1M context) --- tests/bash-guard.test.sh | 46 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/tests/bash-guard.test.sh b/tests/bash-guard.test.sh index aad84e6..c051234 100644 --- a/tests/bash-guard.test.sh +++ b/tests/bash-guard.test.sh @@ -11,6 +11,10 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" GUARD="${REPO_ROOT}/dot_claude/hooks/bash-guard.sh" +# 承認を要する経路で deny を回避できる長さの説明。目的・副作用・可逆性を書けば +# 自然に超える量で、閾値ちょうどの検査は case 8 で別に行う。 +DESC_OK="統合テストを実行する。localhost:5432 の Postgres に接続し public スキーマを truncate して seed を入れ直す。使い捨て DB なので compose down -v で戻せる" + PASS=0 FAIL=0 @@ -22,11 +26,12 @@ require() { } # $1=期待する判定(pass|ask|deny) $2=ラベル $3=コマンド $4=サンドボックス外指定(既定 false) +# $5=description(既定 空)。承認を要する経路では 80 バイト以上ないと deny になる assert_decision() { - local expected="$1" label="$2" command="$3" escape="${4:-false}" + local expected="$1" label="$2" command="$3" escape="${4:-false}" description="${5:-}" local payload got - payload=$(jq -cn --arg c "${command}" --argjson e "${escape}" \ - '{tool_input: {command: $c, dangerouslyDisableSandbox: $e}}') + payload=$(jq -cn --arg c "${command}" --arg d "${description}" --argjson e "${escape}" \ + '{tool_input: {command: $c, description: $d, dangerouslyDisableSandbox: $e}}') got=$(printf '%s' "${payload}" | bash "${GUARD}" 2>/dev/null | jq -r '.hookSpecificOutput.permissionDecision // empty' 2>/dev/null) # 何も返さない = 素通り @@ -76,7 +81,7 @@ assert_decision deny "filter-branch" 'git filter-branch --tree-filter x HEAD' assert_decision deny "gh repo delete" 'gh repo delete foo/bar' echo "case 4: サンドボックス外実行と認証情報の扱いは ask" -assert_decision ask "サンドボックス外" 'go test ./...' true +assert_decision ask "サンドボックス外" 'go test ./...' true "${DESC_OK}" assert_decision ask "gh auth token の埋め込み" 'GH_TOKEN=$(gh auth token) zizmor .' assert_decision ask "gh secret set" 'gh secret set FOO --body bar' assert_decision ask "gh api の書き込み" 'gh api -X DELETE repos/o/r/x' @@ -108,6 +113,39 @@ else FAIL=$((FAIL + 1)) fi +# 承認ダイアログに出るのは description だけなので、説明が薄いまま承認を求めるのを +# 止める。deny なのは理由がモデルに返り、説明を書き直させられるためである。 +# 対象は実際に承認を要する経路に限る。サンドボックス内で完結するコマンドは +# 自動許可でプロンプトが出ないので、ここで縛ると摩擦だけが増える。 +echo "case 8: 承認を要する経路は説明が無いと deny" +assert_decision deny "サンドボックス外・説明なし" 'go test ./...' true +assert_decision deny "サンドボックス外・説明が短い" 'go test ./...' true 'テスト実行' +assert_decision ask "サンドボックス外・説明が十分" 'go test ./...' true "${DESC_OK}" +assert_decision deny "docker exec・説明が短い" 'docker exec -i pg-1 psql -c "select 1"' false 'DB確認' +assert_decision pass "docker exec・説明が十分" 'docker exec -i pg-1 psql -c "select 1"' false "${DESC_OK}" +assert_decision deny "docker compose exec・説明が短い" 'docker compose exec -T db psql' false '確認' +assert_decision deny "docker compose up -d・説明が短い" 'docker compose up -d' false '起動' +assert_decision deny "docker cp・説明が短い" 'docker cp ./x.sql pg-1:/tmp/x.sql' false '転送' + +echo "case 9: 読み取り専用とサンドボックス内は説明を求めない" +assert_decision pass "docker ps" 'docker ps --format json' +assert_decision pass "docker compose ps" 'docker compose ps' +assert_decision pass "docker logs" 'docker logs pg-1 --tail 20' +assert_decision pass "サンドボックス内の npm run" 'npm run test:integration 2>&1 | tail -50' +assert_decision pass "サンドボックス内の go test" 'go test ./... -count=1' +# 単語の一部を拾わないこと。execute や runner は exec / run ではない +assert_decision pass "docker という語を含むだけの文字列" 'grep -rn docker-compose.yml .' + +echo "case 10: 閾値は 80 バイト。境界を跨ぐところで判定が変わる" +DESC_79=$(printf 'a%.0s' $(seq 1 79)) +DESC_80=$(printf 'a%.0s' $(seq 1 80)) +assert_decision deny "79 バイト" 'go test ./...' true "${DESC_79}" +assert_decision ask "80 バイト" 'go test ./...' true "${DESC_80}" + +echo "case 11: 破壊的操作の deny は説明の有無より優先する" +assert_decision deny "force push は説明があっても deny" 'git push --force origin main' false "${DESC_OK}" +assert_decision deny "reset --hard は説明があっても deny" 'git reset --hard HEAD~1' false "${DESC_OK}" + echo echo "pass=${PASS} fail=${FAIL}" [[ "${FAIL}" -eq 0 ]] From 0a784532afd5673a400209a1f27b3990123917c4 Mon Sep 17 00:00:00 2001 From: Yuichi TSUNEMATSU Date: Thu, 3 Sep 2026 10:22:29 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat(claude):=20auto-mode=20=E3=81=AE?= =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E3=82=92=E5=8F=96=E3=82=8A=E8=BE=BC=E3=81=BF?= =?UTF-8?q?=20settings.json=20=E3=82=92=E3=83=86=E3=83=B3=E3=83=97?= =?UTF-8?q?=E3=83=AC=E3=83=BC=E3=83=88=E5=8C=96=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `/auto-mode-setup` が生成した `autoMode` ブロックを管理下に入れる。取り込ま ないままだと `chezmoi apply` が実ファイルから 180 行を消すドリフトが残る。 同時に `settings.json` を `settings.json.tmpl` に変える。`autoMode` の説明文 にホームディレクトリの絶対パスが3箇所あり、そこにアカウント名が入る。この リポジトリは public で、これまで同じ文字列を一度も含んでいない。`{{ .chezmoi.homeDir }}` に置き換えて公開物に残らないようにした。 `chezmoi execute-template` の出力が現行の `~/.claude/settings.json` と完全に 一致することを確認済み。3ファイルとも `chezmoi diff` は空である。 Co-Authored-By: Claude Opus 5 (1M context) --- .../{settings.json => settings.json.tmpl} | 320 ++++++++++-------- 1 file changed, 179 insertions(+), 141 deletions(-) rename dot_claude/{settings.json => settings.json.tmpl} (62%) diff --git a/dot_claude/settings.json b/dot_claude/settings.json.tmpl similarity index 62% rename from dot_claude/settings.json rename to dot_claude/settings.json.tmpl index e6b2743..92852b7 100644 --- a/dot_claude/settings.json +++ b/dot_claude/settings.json.tmpl @@ -1,31 +1,4 @@ { - "hooks": { - "SessionStart": [ - { - "matcher": "*", - "hooks": [ - { - "type": "command", - "command": "bash \"$HOME/.claude/hooks/herdr-agent-state.sh\" session", - "timeout": 10 - } - ] - } - ], - "PreToolUse": [ - { - "matcher": "Bash", - "hooks": [ - { - "type": "command", - "command": "bash \"$HOME/.claude/hooks/bash-guard.sh\"", - "timeout": 10, - "statusMessage": "bash-guard" - } - ] - } - ] - }, "permissions": { "allow": [ "Bash(git *)", @@ -57,14 +30,6 @@ "mcp__claude_ai_Notion__notion-fetch", "mcp__claude_ai_Atlassian_Rovo__getJiraIssue" ], - "ask": [ - "Bash(terraform apply*)", - "Bash(terraform destroy*)", - "Bash(kubectl apply*)", - "Bash(kubectl delete*)", - "Bash(docker push*)", - "Bash(gh pr merge*)" - ], "deny": [ "Read(**/.env)", "Read(**/.env.local)", @@ -97,71 +62,94 @@ "Bash(git filter-repo*)", "Bash(git filter-branch*)", "Bash(gh repo delete*)" + ], + "ask": [ + "Bash(terraform apply*)", + "Bash(terraform destroy*)", + "Bash(kubectl apply*)", + "Bash(kubectl delete*)", + "Bash(docker push*)", + "Bash(gh pr merge*)" ] }, + "hooks": { + "SessionStart": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "bash \"$HOME/.claude/hooks/herdr-agent-state.sh\" session", + "timeout": 10 + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "bash \"$HOME/.claude/hooks/bash-guard.sh\"", + "timeout": 10, + "statusMessage": "bash-guard" + } + ] + } + ] + }, + "statusLine": { + "type": "command", + "command": "~/.claude/statusline.py" + }, + "enabledPlugins": { + "crit@crit": true, + "frontend-design@claude-plugins-official": true, + "gopls-lsp@claude-plugins-official": true, + "security-guidance@claude-plugins-official": true, + "superpowers@claude-plugins-official": true, + "swift-lsp@claude-plugins-official": true, + "dart-flutter@dart-flutter": true, + "modern-web-guidance@googlechrome": true, + "claude-security@claude-plugins-official": true + }, + "extraKnownMarketplaces": { + "datadog-pup": { + "source": { + "source": "github", + "repo": "datadog-labs/pup" + } + }, + "superpowers-marketplace": { + "source": { + "source": "github", + "repo": "obra/superpowers-marketplace" + } + }, + "crit": { + "source": { + "source": "github", + "repo": "tomasz-tomczyk/crit" + } + }, + "dart-flutter": { + "source": { + "source": "github", + "repo": "flutter/agent-plugins" + } + }, + "googlechrome": { + "source": { + "source": "github", + "repo": "GoogleChrome/modern-web-guidance" + } + } + }, "sandbox": { "enabled": true, "autoAllowBashIfSandboxed": true, "allowUnsandboxedCommands": true, - "excludedCommands": [ - "git *", - "docker *", - "docker-compose *", - "gh *", - "chezmoi *", - "gcloud *", - "bq *", - "gsutil *", - "aws *", - "kubectl *", - "terraform *" - ], - "filesystem": { - "denyRead": [ - "~/.aws/", - "~/.ssh/", - "~/.netrc", - "~/.config/gcloud/", - "~/.config/gh/", - "~/.kube/", - "**/.env", - "**/.env.local", - "**/.env.*.local", - "**/.env.dev*", - "**/.env.development*", - "**/.env.stg*", - "**/.env.staging*", - "**/.env.prod*", - "**/.env.production*", - "**/.env.test*", - "**/.env_*", - "**/.envrc", - "**/.envrc.local", - "**/secrets/**", - "**/credentials.json", - "**/credentials.yml", - "**/credentials.yaml", - "**/credentials.db", - "**/*.pem", - "**/*.key" - ], - "allowWrite": [ - "/var/folders/*/*/T", - "~/.npm", - "~/.pnpm-store", - "~/go/pkg", - "~/Library/Caches/go-build", - "~/Library/Caches/golangci-lint", - "~/Library/Caches/Yarn", - "~/Library/Caches/mise", - "~/.gradle/caches", - "~/.gradle/wrapper", - "~/.m2/repository", - "~/.cargo/registry", - "~/.cargo/git", - "~/.cache" - ] - }, "network": { "allowedDomains": [ "github.com", @@ -206,55 +194,105 @@ "*.claude.ai" ], "allowLocalBinding": true - } - }, - "statusLine": { - "type": "command", - "command": "~/.claude/statusline.py" - }, - "enabledPlugins": { - "crit@crit": true, - "frontend-design@claude-plugins-official": true, - "gopls-lsp@claude-plugins-official": true, - "security-guidance@claude-plugins-official": true, - "superpowers@claude-plugins-official": true, - "swift-lsp@claude-plugins-official": true, - "dart-flutter@dart-flutter": true, - "modern-web-guidance@googlechrome": true, - "claude-security@claude-plugins-official": true - }, - "extraKnownMarketplaces": { - "datadog-pup": { - "source": { - "source": "github", - "repo": "datadog-labs/pup" - } - }, - "superpowers-marketplace": { - "source": { - "source": "github", - "repo": "obra/superpowers-marketplace" - } }, - "crit": { - "source": { - "source": "github", - "repo": "tomasz-tomczyk/crit" - } - }, - "dart-flutter": { - "source": { - "source": "github", - "repo": "flutter/agent-plugins" - } + "filesystem": { + "allowWrite": [ + "/var/folders/*/*/T", + "~/.npm", + "~/.pnpm-store", + "~/go/pkg", + "~/Library/Caches/go-build", + "~/Library/Caches/golangci-lint", + "~/Library/Caches/Yarn", + "~/Library/Caches/mise", + "~/.gradle/caches", + "~/.gradle/wrapper", + "~/.m2/repository", + "~/.cargo/registry", + "~/.cargo/git", + "~/.cache" + ], + "denyRead": [ + "~/.aws/", + "~/.ssh/", + "~/.netrc", + "~/.config/gcloud/", + "~/.config/gh/", + "~/.kube/", + "**/.env", + "**/.env.local", + "**/.env.*.local", + "**/.env.dev*", + "**/.env.development*", + "**/.env.stg*", + "**/.env.staging*", + "**/.env.prod*", + "**/.env.production*", + "**/.env.test*", + "**/.env_*", + "**/.envrc", + "**/.envrc.local", + "**/secrets/**", + "**/credentials.json", + "**/credentials.yml", + "**/credentials.yaml", + "**/credentials.db", + "**/*.pem", + "**/*.key" + ] }, - "googlechrome": { - "source": { - "source": "github", - "repo": "GoogleChrome/modern-web-guidance" - } - } + "excludedCommands": [ + "git *", + "docker *", + "docker-compose *", + "gh *", + "chezmoi *", + "gcloud *", + "bq *", + "gsutil *", + "aws *", + "kubectl *", + "terraform *" + ] }, "skipWorkflowUsageWarning": true, - "preferredNotifChannel": "ghostty" + "preferredNotifChannel": "ghostty", + "autoMode": { + "environment": [ + "### Org-wide", + "**Organization**: None configured", + "**Cloud provider(s)**: None configured", + "**Repository visibility**: assume private unless the remote host and repo name indicate otherwise, or a visibility check in the transcript shows public", + "**Internal sharing / snippet hosting**: None configured — treat public paste/gist services as outside the trust boundary", + "**Secrets management**: None configured", + "**Default / protected branches**: unknown — origin/HEAD unset and no remotes configured; the repo's own docs say to branch from the default branch tracked to remote, but no branch name or protection data is available", + "**CI/CD deploy targets**: None configured", + "**Network posture**: None configured", + "**Source control**: The trusted repo and its remote(s) only (no additional orgs configured) — as scoped by the Trusted repo and Repository visibility entries above; note this checkout ({{ .chezmoi.homeDir }}) has no configured remotes", + "**Trusted internal domains**: None configured", + "**Trusted cloud buckets**: None configured", + "**Key internal services**: None configured", + "**Internal package registry**: None configured", + "**Sensitive data locations & audiences**: any file or store holding personal data, confidential business data, credentials, regulated data, or similarly sensitive material; preserve exact handles when known and share only with audiences cleared at the [named+specifics] bar — note this working directory is the home directory itself, so dotfiles, shell history, and credential files sit inside the working tree", + "**Data retention / declassification**: None configured", + "**Sensitive remote targets**: any namespace, host, or container whose name carries `prod` or `production` as a whole word or name segment (hyphen/underscore/dot-delimited — e.g. matches `prod-db`, not `producer`)", + "**Protected deployment namespaces / environments**: None configured — fall back to the Sensitive remote targets heuristic", + "**Protected IaC scopes**: IAM, RBAC, networking, quota, and node-pool resources; anything whose name or tag carries `prod` or `production` as a whole word or name segment", + "### User-specific", + "**Primary use of Claude Code**: software development, plus Japanese-language technical writing and editing (book chapters, articles, documentation) per the user's own CLAUDE.md", + "**Trusted repo**: The git repository the agent started in (its working directory) and its configured remote(s). When the repo's public/private visibility is given — by the Repository visibility entry or the user's own message — use it to scope what is OK to commit or push there: confidential material is fine in a private repo; in a public one, only that repo's own work is — and content ported, repointed, or first read from outside this session's repo is not its own work, whoever directed the port. Visibility scopes confidential material only: secrets and sensitive data (personal & entrusted) are never cleared into any repo by its visibility (see Definitions). Here that repo is {{ .chezmoi.homeDir }} with no remotes and zero tracked files.", + "**Org-specific CLIs**: chezmoi (dotfile management; 8x in this project's transcripts, 2x in shell history) — routine local dotfile read/apply operations under this home directory only", + "**Routine under this repo's prefix**: local-only development and writing work under {{ .chezmoi.homeDir }}; commands are expected to run inside the sandbox, with temp files in $TMPDIR rather than /tmp" + ], + "allow": [ + "$defaults", + "Bash(chezmoi diff:*)", + "Bash(chezmoi status:*)" + ], + "soft_deny": [ + "$defaults", + "Bash(chezmoi apply:*)", + "Bash(chezmoi forget:*)" + ] + } }