From 1640bb83e190cfabc0e64fd5676d08c0926b4f2a Mon Sep 17 00:00:00 2001 From: Sudachi-kun Date: Sun, 6 Sep 2026 17:51:36 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20Bash=20=E5=84=AA=E5=85=88=E6=8C=87?= =?UTF-8?q?=E7=A4=BA=E3=82=92=E8=A7=A3=E9=99=A4=E3=81=99=E3=82=8B=20native?= =?UTF-8?q?-file-tools=20=E3=83=95=E3=83=83=E3=82=AF=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0=EF=BC=88v0.78.0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude Code 2.1.261 が auto / bypassPermissions セッションに注入する Bash 優先指示(未文書化 flag CLAUDE_CODE_THRIFTY_SONIC)で、Edit|Write フック・paths 付き Rules・nested CLAUDE.md・native rewind が無音で 動かなくなる問題を実 CLI で再現し、env-only feature として CLAUDE_CODE_THRIFTY_SONIC=0 を配布する(Standard / Full 既定有効)。 - features/native-file-tools を追加し、registry / profiles / wizard / i18n / MDM 許可キー / docs / CHANGELOG 0.78.0 に登録 - 後発キーの補完を _fill_late_feature_defaults に集約し、custom プロファイルの保存設定再利用で agent-teams の env が配布されない 既存バグも修正 - 回帰テスト tests/unit/test-native-file-tools.sh(修正前 12 件 FAIL)と 実 CLI 検証ハーネス tests/manual/bash-first-steer/(CI 非連動)を追加 - README(日英)に security-guidance 編集時警告の適用条件と native rewind が Bash 編集を追跡しない注記を追加 --- CHANGELOG.md | 17 + CLAUDE.md | 1 + README.en.md | 23 +- README.md | 33 +- commands/checkpoint.md | 4 + docs/mdm/README.md | 1 + docs/wizard-config-mapping.en.md | 1 + docs/wizard-config-mapping.md | 1 + features/native-file-tools/feature.json | 14 + features/native-file-tools/hooks.json | 5 + i18n/en/strings.sh | 2 + i18n/ja/strings.sh | 2 + lib/features.sh | 3 +- mdm/install-mdm.sh | 4 +- profiles/full.conf | 1 + profiles/minimal.conf | 1 + profiles/standard.conf | 1 + tests/manual/bash-first-steer/README.md | 107 ++++++ tests/manual/bash-first-steer/make-fixture.sh | 96 +++++ .../manual/bash-first-steer/observer-hook.sh | 25 ++ tests/manual/bash-first-steer/run-case.sh | 162 +++++++++ .../manual/bash-first-steer/summarize-run.sh | 62 ++++ tests/unit/test-docs-sync.sh | 2 +- tests/unit/test-native-file-tools.sh | 335 ++++++++++++++++++ wizard/defaults.conf | 1 + wizard/registry.sh | 14 + wizard/steps.sh | 3 +- wizard/wizard.sh | 10 +- 28 files changed, 914 insertions(+), 17 deletions(-) create mode 100644 features/native-file-tools/feature.json create mode 100644 features/native-file-tools/hooks.json create mode 100644 tests/manual/bash-first-steer/README.md create mode 100644 tests/manual/bash-first-steer/make-fixture.sh create mode 100644 tests/manual/bash-first-steer/observer-hook.sh create mode 100644 tests/manual/bash-first-steer/run-case.sh create mode 100644 tests/manual/bash-first-steer/summarize-run.sh create mode 100644 tests/unit/test-native-file-tools.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b8dbf9..723ab88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/). +## [0.78.0] - 2026-09-06 + +### Added +- **Bash 優先指示の解除フック `native-file-tools` を追加(Standard / Full 既定有効)**: Claude Code 2.1.261 は auto / bypassPermissions モードのセッションに「ファイルの読み書きは Read/Edit/Write ではなく cat / sed / heredoc で行え」という指示を注入する(バイナリ内の未文書化 feature flag `CLAUDE_CODE_THRIFTY_SONIC`。Fable 5.1 系モデルでは強制有効、Opus 5 は GrowthBook のコホート配信、それ以外のモデルは既定で無効だが、サーバー配信の clientData やモデル別 capability からも強制され得る条件式になっている)。この状態ではモデルが Read/Edit/Write をほぼ使わないため、キットの `PostToolUse` `Edit|Write`(prettier-hooks / biome-hooks)、`PreToolUse` `Write`(doc-blocker)、`PostToolUse` `Write`(doc-size-guard)が発火せず、`paths:` 付き Rules とサブディレクトリの CLAUDE.md も読み込まれず、native checkpoint / rewind の追跡対象にもならない。新フックは `settings.json` の `env` に `CLAUDE_CODE_THRIFTY_SONIC="0"` を入れて指示の注入を解除する + - **実 CLI で再現・修正確認済み**: Claude Code 2.1.261 / claude-fable-5-1 / `--permission-mode auto` / `--setting-sources project`(利用者のグローバル設定・プラグイン・MCP を読み込まない隔離)で、同一の合成リポジトリと課題を新規セッションで比較した。フラグ未指定では 3 セッション中 3 セッションがツール呼び出しを Bash のみで済ませ、Biome 整形なし・doc-blocker の確認なし・doc-size-guard の警告なし・`InstructionsLoaded` イベントは session_start の 2 件のみ(paths Rules / nested CLAUDE.md の遅延ロードなし)。`env.CLAUDE_CODE_THRIFTY_SONIC="0"` では 3 セッション中 3 セッションが Read / Write を使い、Biome 整形・doc-blocker の `ask`・doc-size-guard の警告・`path_glob_match` / `nested_traversal` のロードがすべて観測された。トランスクリプトに永続化される `auto_mode` attachment の `bashFirst` 値(true / 消失)でも指示の有無を直接確認した。キットが生成した `settings.json` をそのまま使った検証でも同じ結果を得た + - **保証範囲と残る制約**: 復元されるのは通常のツール選択であって、Bash の使用が禁止されるわけではない。利用者が「Bash で編集して」と指示した対照ケースでは、フラグを入れても Bash のみで編集され、フック・paths Rules は従来どおり発火しない。auto / bypassPermissions 以外のモードでは元々この指示は注入されない(バイナリの条件式が他モードで即座に空を返す。`acceptEdits` は `"1"` を入れても注入されないことを実測で確認、`default` / `plan` は条件式からの判断で実測はしていない)。doc-blocker / doc-size-guard が Edit に反応しない点は従来からの制限で、今回の対象外 + - **配布形態**: `agent-teams` / `no-flicker` と同じ env-only feature(`features/native-file-tools/hooks.json`)。`ENABLE_NATIVE_FILE_TOOLS` をレジストリ・profiles・wizard(フック選択の `native-tools` トークン、確認画面、非対話デフォルト)・MDM の許可キーに登録した。Standard / Full は既定 `true`、Minimal は `false`。既存インストールは `setup.sh --update` / 自動アップデートでプロファイル既定を受け取り、custom プロファイルは agent-teams と同じく `true` で補完する。`~/.claude-starter-kit.conf` に明示的に書かれた値は上書きしない。Claude Code 側でフラグが撤去された場合はフラグメントからキーを外すだけでよく、次回アップデートの 3-way merge がこのキーを変更していない利用者の `settings.json` から除去する(#120 の effortLevel 撤去と同じ kit-removed 経路。自分で値を変更していた利用者の値は従来どおり保持され、残っても未知の env は Claude Code に無視される) + - **回帰テスト**: `tests/unit/test-native-file-tools.sh`(Standard / Full の生成 `settings.json` に env キーが入ること、Minimal と明示 `false` では入らないこと、更新経路でのプロファイル既定補完と明示値の保持、`--hooks` トークン)。修正前のコードでは 12 件が失敗する + - **実 CLI 検証ハーネスを同梱(CI 非連動)**: `tests/manual/bash-first-steer/` に合成 fixture 生成・ケース実行・集計スクリプトと期待結果表を置いた。モデルを実際に呼ぶため通常 CI には組み込まない + +### Changed +- **README のセキュリティ機能・native rewind の説明を実測に合わせて修正**: security-guidance の「編集時」パターン警告は Edit / Write ツール経由の変更にだけ働き、Bash 経由の変更はターン終了時の git diff レビューとコミット時レビューで拾うことを明記した(導入版 2.0.7 の `hooks.json` / `security_reminder_hook.py` で確認)。コンパクト前スナップショットの説明と `commands/checkpoint.md` に、native rewind はファイル編集ツール(Edit / Write / NotebookEdit)による変更しか復元しない(公式ドキュメント「Checkpointing」の Limitations)ことを追記した +- 後発キーのデフォルト補完(agent-teams / native-file-tools)を `wizard/registry.sh` の `_fill_late_feature_defaults` に集約し、更新経路(`_restore_config_from_manifest`)・非対話 fresh install(`_fill_noninteractive_defaults`)・対話ウィザードの保存設定再利用(`fill_missing_profile_defaults`)から呼ぶようにした + +### Fixed +- **custom プロファイルで `setup.sh` を直接再実行し保存設定を再利用すると、agent-teams(と今回の native-file-tools)の env が配布されない問題を修正**: 対話ウィザードの「保存された設定を再利用する」分岐は `fill_missing_profile_defaults` でプロファイル conf を読むだけで、conf を持たない custom プロファイルでは後発キーが空のままになり、`_feature_deploy_enabled` が無効扱いして env フラグメントを出力していなかった(#138 の修正は `setup.sh --update` 経路のみを対象にしていた)。`install.sh` 経由の再実行は常に `--update` が付くため影響せず、README が案内する「リポジトリ更新後に `./setup.sh` を再実行」の手順でのみ発生する。`fill_missing_profile_defaults` からも `_fill_late_feature_defaults` を呼ぶようにし、回帰テストを追加した。保存済みの明示的な `false` は引き続き保持される + ## [0.77.1] - 2026-08-31 ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md index 67330d6..496890b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -272,6 +272,7 @@ Multiple features can safely use the same hook type (e.g., `PreCompact`, `PostCo - **Status Line**: Python statusLine command showing model, context usage, and 5h/7d rate limits. - **Doc Size Guard**: non-blocking size-hygiene warning when CLAUDE.md/AGENTS.md exceeds line targets after Write. - **Feature Recommendation**: writes pending feature names and notifies via SessionStart for enabled profiles. +- **Native File Tools**: env-only feature (`CLAUDE_CODE_THRIFTY_SONIC=0`, Standard/Full) that opts out of Claude Code's Bash-first steer in auto/bypassPermissions sessions; without it Fable 5.1 reads/edits via cat/sed/heredocs (0 of 3 reproduction sessions used Read/Edit/Write) so the Edit|Write hooks, `paths:` rules and nested CLAUDE.md do not fire. Real-CLI harness (model-calling, not in CI): `tests/manual/bash-first-steer/`. ## Platform Detection diff --git a/README.en.md b/README.en.md index e8394da..73fca00 100644 --- a/README.en.md +++ b/README.en.md @@ -55,7 +55,7 @@ Claude Code Starter Kit bootstraps a consistent, high-quality Claude Code enviro - **8 rules**: coding-style, git-workflow, performance, security, testing, agents, anti-patterns, permissions-guide - **21 slash commands**: /plan, /tdd, /build-fix, /e2e, /verify, /research, /web-article, /oss-analyze, /web-source-review, /handover, /update-kit, and more - **13 skill modules**: backend-patterns, frontend-patterns, security-review, tdd-workflow, prompt-patterns, cloudnative-writing-baseline (Japanese business writing), and more -- **11 optional hooks/settings**: safety net (cc-safety-net), auto update, web content update, tmux reminder, doc blocker, Prettier or Biome formatting, PR creation log, pre-compact snapshot (opt-in), statusline, doc size guard, feature recommendation +- **12 optional hooks/settings**: safety net (cc-safety-net), auto update, web content update, tmux reminder, doc blocker, Prettier or Biome formatting, PR creation log, pre-compact snapshot (opt-in), statusline, doc size guard, feature recommendation, native file tools (keeps Read/Edit/Write in use under auto mode so the edit hooks and path-scoped rules keep firing) - **15 plugins** from multiple marketplaces: security-guidance, commit-commands, pr-review-toolkit, feature-dev, code-review, claude-md-management, superpowers, code-simplifier, document-skills, example-skills, typescript-lsp, gopls-lsp, pyright-lsp, rust-analyzer-lsp, claude-security - **i18n**: English & Japanese - **Codex Plugin** sub-agent integration (optional, supports ChatGPT sign-in or OpenAI API key auth) @@ -84,7 +84,7 @@ The kit's security capabilities cover four different layers; they complement rat | Layer | Component | Activation | Profile | |---|---|---|---| | Always-on guardrails | `rules/security.md` + `config/permissions.json` (all profiles) + safety-net (Standard / Full) | Always | All (safety-net: Standard / Full) | -| Automatic review | **security-guidance** plugin | On edits, turn completion, and commits | Standard / Full | +| Automatic review | **security-guidance** plugin | On edits, turn completion, and commits. The edit-time pattern warnings only see changes made with the editing tools (Edit / Write / MultiEdit / NotebookEdit); Bash edits are covered later by the turn-completion git-diff review and the commit review | Standard / Full | | One-off review | Claude Code's built-in `/security-review` | Manual, for the current branch diff | Not managed by the kit | | Deep scan | **claude-security** plugin | Manual, with `/claude-security` | Full | @@ -233,6 +233,7 @@ Hooks are automated safety checks that run automatically when Claude Code execut | Doc Size Guard | Warns when CLAUDE.md/AGENTS.md exceeds size-hygiene targets (non-blocking; Full only) | | Web Content Update | Auto-updates the web-content-extraction skill's deps on session start (opt-in; default in Full only) | | Feature Recommendation | Notifies about newly available features for the selected profile | +| Native File Tools | Keeps Read/Edit/Write as the primary file tools in auto / bypassPermissions sessions so the Edit\|Write hooks above, path-scoped rules, nested CLAUDE.md and native rewind keep working (default in Standard / Full) | #### Safety Net @@ -265,6 +266,22 @@ Automatically checks for new starter kit releases on GitHub on both `SessionStar > **Enabled by default in Standard / Full profiles.** Disable with `ENABLE_AUTO_UPDATE=false` in the hooks selection. +#### Native File Tools + +Claude Code 2.1.261 injects a "Bash-first" instruction into **auto / bypassPermissions** sessions: read files with cat / sed and change them with sed / heredocs instead of the Read, Edit and Write tools (undocumented feature flag `CLAUDE_CODE_THRIFTY_SONIC`; forced on for Fable 5.1 models, cohort-gated for Opus 5). Under that instruction the model almost never calls Read/Edit/Write, so the following stop working **silently** (reproduced with real CLI sessions; see `tests/manual/bash-first-steer/README.md`): + +- the `PostToolUse` `Edit|Write` formatters (Prettier / Biome), the `PreToolUse` `Write` doc blocker and the `PostToolUse` `Write` doc size guard +- `.claude/rules/*.md` with `paths:` and nested `CLAUDE.md` files (they load when Claude *reads* a matching file, not when it runs cat) +- native checkpoint / rewind tracking, and security-guidance's edit-time pattern warnings + +This hook writes `CLAUDE_CODE_THRIFTY_SONIC=0` into the `env` block of `settings.json`, which removes the instruction. + +- **What it guarantees**: normal tool selection comes back (on the same task, 3 of 3 sessions used Bash only before the fix and 3 of 3 used Read/Write after). It does not forbid Bash: a deliberate Bash edit, or a prompt that asks for one, still bypasses the hooks, path-scoped rules and rewind +- **Other modes**: outside auto / bypassPermissions the instruction is not injected (the gating code returns nothing for any other mode; acceptEdits was also verified empirically, default / plan follow from the code), so the setting is harmless there +- **Disable**: deselect it in the wizard's hook list, leave `native-tools` out of `--hooks`, or set `ENABLE_NATIVE_FILE_TOOLS=false` in `~/.claude-starter-kit.conf`. If Claude Code retires the flag, removing the key from the kit fragment lets the next update's 3-way merge drop it from the `settings.json` of every user who never changed the value (a user-modified value is kept, as usual; a leftover key is harmless because Claude Code ignores unknown env keys) + +> **Enabled by default in Standard / Full profiles.** Existing installs pick it up on `setup.sh --update` / auto update unless `~/.claude-starter-kit.conf` explicitly says `false`. Native rewind itself only restores changes made through Claude's file editing tools (Edit, Write, NotebookEdit); Bash edits (`sed -i`, heredocs) are never tracked (see Limitations in the official Checkpointing doc). + ## Usage > **Important: You must restart your terminal after setup.** @@ -362,7 +379,7 @@ NONINTERACTIVE=1 bash -c "$(curl -fsSL https://raw.githubusercontent.com/cloudna --new-init=true \ --codex-plugin=false \ --commit-attribution=false \ - --hooks=safety-net,auto-update,tmux,prettier,pr-log,pre-commit,agent-teams \ + --hooks=safety-net,auto-update,tmux,prettier,pr-log,pre-commit,agent-teams,native-tools \ --plugins=security-guidance,commit-commands,pr-review-toolkit,document-skills@anthropic-agent-skills # Reuse a saved config diff --git a/README.md b/README.md index c15f2b8..e484a6f 100644 --- a/README.md +++ b/README.md @@ -470,7 +470,7 @@ URL・公式ドキュメント・ブログ・ニュース・OSS ページを読 - **セキュリティ**: SSRF 多層防御(http(s) 限定・内部/プライベート IP 拒否・接続 IP pin・リダイレクト各ホップ検査)、非フェッチ DOM(外部サブリソース取得・スクリプト実行なし)、PDF は CJK 対応・解凍爆弾対策付き。開発時のみ `ALLOW_PRIVATE_URLS=true` で内部 URL を許可。 - **依存の自動更新(opt-in)**: `web-content-update` フックが SessionStart でスキルの依存(defuddle/jsdom/pdfjs-dist/undici)を更新(24h スロットル・テストゲート+ロールバック)。**Full のみ既定有効**、Standard では opt-in。手動更新は `npm run update:deps`。 -### 🪝 フック(安全装置・11個) +### 🪝 フック(安全装置・12個) フックは **自動で動作する安全装置** です。コードを書いたり保存したりしたときに、自動でチェックが走ります。 @@ -487,6 +487,7 @@ URL・公式ドキュメント・ブログ・ニュース・OSS ページを読 | ドキュメントサイズガード | CLAUDE.md/AGENTS.md の肥大化を警告(非ブロック・Full のみ) | | Web 取得スキル更新 | web-content-extraction スキルの依存を起動時に自動更新(opt-in・Full のみ既定有効) | | 機能レコメンド | 選択プロファイルで利用可能になった新機能を通知 | +| ネイティブファイルツール | auto / bypassPermissions モードでも Read/Edit/Write を優先させ、上記の Edit\|Write 系フック・paths 付き Rules・ネストした CLAUDE.md・native rewind が効く状態を保つ(Standard / Full 既定有効) | #### Safety Net とは? @@ -559,7 +560,31 @@ compact 実行(コンテキスト圧縮) 復元したい場合は `git stash list` で `pre-compact snapshot` を探して `git stash apply` してください。git リポジトリ外のプロジェクトでは何も起きません(エラーにはなりません)。 -> **全プロファイルでデフォルト無効(opt-in)です。** 現行の Claude Code はネイティブの checkpoint / rewind を備えており、通常はこのフックは不要です。圧縮前の状態を git 側にも残したい場合のみ、ウィザードのフック選択で有効化してください。過去に有効化した設定(保存済み config)はアップデートで上書きされません。 +> **全プロファイルでデフォルト無効(opt-in)です。** 現行の Claude Code はネイティブの checkpoint / rewind を備えており、通常はこのフックは不要です。圧縮前の状態を git 側にも残したい場合のみ、ウィザードのフック選択で有効化してください。過去に有効化した設定(保存済み config)はアップデートで上書きされません。なお native rewind が復元できるのはファイル編集ツール(Edit / Write / NotebookEdit)で行った変更だけで、Bash(`sed -i` や heredoc)で行った変更は追跡されません(公式ドキュメント「Checkpointing」の Limitations に記載)。 + +#### ネイティブファイルツールとは? + +Claude Code 2.1.261 は、**auto / bypassPermissions モード**のセッションに「ファイルの読み書きは Read/Edit/Write ではなく cat / sed / heredoc など Bash で行え」という指示(未文書化の feature flag `CLAUDE_CODE_THRIFTY_SONIC`。Fable 5.1 では強制有効、Opus 5 はコホート配信)を注入します。この状態では、モデルが Read/Edit/Write をほぼ使わなくなるため、次の仕組みが**無音で動かなくなります**(実 CLI で再現確認済み・詳細は `tests/manual/bash-first-steer/README.md`)。 + +- `PostToolUse` `Edit|Write` の Prettier / Biome 自動フォーマット、`PreToolUse` `Write` の Doc ブロッカー、`PostToolUse` `Write` のドキュメントサイズガード +- `paths:` 付きの Rules とサブディレクトリの CLAUDE.md(Read したときに読み込まれる仕組みのため、cat では読み込まれない) +- native checkpoint / rewind による変更追跡、security-guidance プラグインの編集時パターン警告 + +このフックは `CLAUDE_CODE_THRIFTY_SONIC=0` を `settings.json` の `env` に入れて指示の注入を解除します。 + +``` +auto モードのセッション開始 + ↓ +env.CLAUDE_CODE_THRIFTY_SONIC="0" が設定されている + ├── はい → Bash 優先の指示は注入されない → Read/Edit/Write が通常どおり選ばれ、フック・Rules が発火 + └── いいえ → 指示が注入される → cat/sed/heredoc で読み書き → フック・paths Rules・rewind は素通り +``` + +- **保証範囲**: 通常のツール選択が戻ることです(同一課題で修正前 3 セッション中 3 セッションが Bash のみ、修正後 3 セッション中 3 セッションが Read/Write を使用)。Bash の使用を禁止するものではなく、モデルが意図的に Bash で編集した場合や、利用者が「Bash で編集して」と指示した場合は、従来どおりフック・paths Rules・rewind の対象外です +- **auto / bypassPermissions 以外のモード**では、この指示は元々注入されません(バイナリ内の条件式は auto / bypassPermissions 以外で即座に空を返す。acceptEdits は実測でも未注入を確認、default / plan は条件式からの判断)。設定を入れても害はありません +- **無効化**: ウィザードのフック選択、`--hooks` から `native-tools` を外す、または `~/.claude-starter-kit.conf` の `ENABLE_NATIVE_FILE_TOOLS=false`。Claude Code 側でこのフラグが撤去された場合は、キットのフラグメントからキーを外すだけで、次回アップデート時の 3-way merge がこのキーを変更していない利用者の `settings.json` から除去します(値を自分で変更していた利用者は従来どおり自分の値が保持される。未知の env は Claude Code に無視されるため残っても無害) + +> **Standard / Full プロファイルでデフォルト有効です。** 既存のインストールは `setup.sh --update` / 自動アップデートで有効になります(`~/.claude-starter-kit.conf` に明示的に `false` と書かれている場合は上書きしません)。 ### 🧩 プラグイン(15個・マルチマーケットプレイス対応) @@ -591,7 +616,7 @@ Standard / Full プロファイルではおすすめのプラグインが自動 | 層 | 実体 | 動き方 | プロファイル | |---|---|---|---| | 常時ガードレール | `rules/security.md` + `config/permissions.json`(全プロファイル)+ safety-net(Standard / Full) | 常時 | 全部(safety-net は Standard / Full) | -| 自動レビュー | **security-guidance** プラグイン | 自動(編集時・ターン終了時・コミット時) | Standard / Full | +| 自動レビュー | **security-guidance** プラグイン | 自動(編集時・ターン終了時・コミット時)。編集時のパターン警告は編集ツール(Edit / Write / MultiEdit / NotebookEdit)経由の変更にだけ働き、Bash 経由の変更はターン終了時の git diff レビューとコミット時レビューで拾う | Standard / Full | | 単発レビュー | Claude Code 組み込みの `/security-review` | 手動(現在のブランチの差分) | キット非管理 | | 深掘りスキャン | **claude-security** プラグイン | 手動(`/claude-security`) | Full | @@ -889,7 +914,7 @@ NONINTERACTIVE=1 bash -c "$(curl -fsSL https://raw.githubusercontent.com/cloudna --codex-plugin=false \ --commit-attribution=false \ --ghostty=true \ - --hooks=safety-net,auto-update,tmux,prettier,pr-log,pre-commit,agent-teams \ + --hooks=safety-net,auto-update,tmux,prettier,pr-log,pre-commit,agent-teams,native-tools \ --plugins=security-guidance,commit-commands,pr-review-toolkit,document-skills@anthropic-agent-skills ``` diff --git a/commands/checkpoint.md b/commands/checkpoint.md index 5f31fce..fb938a3 100644 --- a/commands/checkpoint.md +++ b/commands/checkpoint.md @@ -4,6 +4,10 @@ Record cross-session git milestones. In-session rollback is handled by Claude Code's native checkpoint/rewind (Esc Esc / /rewind) — use this command only for multi-day milestones you want recorded in git. +Native rewind only restores files changed through Claude's file editing tools +(Edit, Write, NotebookEdit). Changes made with Bash (sed -i, heredocs, mv, cp) +are not tracked; rely on git for those. + ## Usage `/checkpoint [create|verify|list] [name]` diff --git a/docs/mdm/README.md b/docs/mdm/README.md index 945f13e..b1aca03 100644 --- a/docs/mdm/README.md +++ b/docs/mdm/README.md @@ -116,6 +116,7 @@ production では対象のローカルアカウントと UID 501 以上が必要 | `ENABLE_BIOME_HOOKS` | Biome による JS / TS 自動整形・lint hook | boolean。必要な Biome runtime は required component として検査する | | `ENABLE_PR_CREATION_LOG` | PR 作成後の URL 記録と review command 案内 hook | boolean。未指定時はプロファイル既定に従う | | `ENABLE_AGENT_TEAMS` | Claude Code の experimental Agent Teams 設定 | boolean。`CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` を有効化する | +| `ENABLE_NATIVE_FILE_TOOLS` | auto / bypassPermissions セッションでも Read/Edit/Write を優先させる設定 | boolean。`CLAUDE_CODE_THRIFTY_SONIC=0` を settings.json の env に入れる。未指定時はプロファイル既定(Standard / Full は true、Minimal は false)に従う | | `ENABLE_STATUSLINE` / `ENABLE_SAFETY_NET` / `ENABLE_DOC_SIZE_GUARD` / `ENABLE_FEATURE_RECOMMENDATION` / `ENABLE_PRE_COMPACT_COMMIT` / `ENABLE_NO_FLICKER` / `ENABLE_NEW_INIT` | その他 feature toggle | 未指定時はプロファイル既定に従う | `ENABLE_GHOSTTY_SETUP` / `ENABLE_FONTS_SETUP` だけは `PROFILE` にかかわらず MDM 既定 `false` で、CLI または管理設定ファイルによる明示的な `true` で opt-in できる。Ghostty の opt-in は、MDM が署名済みで `com.apple.quarantine` のない `/Applications/Ghostty.app` を先に配布することが前提で、対象ユーザーとして動く `setup.sh` は Homebrew cask の導入を試みず、署名・quarantine 検証とユーザー設定だけを行う。これにより非 admin アカウントで password / Gatekeeper prompt が発生しない。キットの auto-update、web updater、通常の marketplace plugin、Codex Plugin は MDM では常に無効にし、更新は新しい40桁 SHAを指定した MDM 再配布で行う。`SELECTED_PLUGINS` は MDM の許可キーではなく、指定すると `exit 50`。fresh / update とも profile preset や保存済みユーザー設定からこれらを再有効化しない。 diff --git a/docs/wizard-config-mapping.en.md b/docs/wizard-config-mapping.en.md index 4960ca2..a158e8d 100644 --- a/docs/wizard-config-mapping.en.md +++ b/docs/wizard-config-mapping.en.md @@ -71,6 +71,7 @@ These flags are used to merge the corresponding `features/*/hooks.json` fragment | `ENABLE_STATUSLINE` | Statusline | Statusline feature toggle | Yes | | `ENABLE_DOC_SIZE_GUARD` | Doc Size Guard | Warn when `CLAUDE.md` / `AGENTS.md` is too large | Yes | | `ENABLE_AGENT_TEAMS` | Agent Teams (experimental) | Injects CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 into settings.json env (default true in all profiles) | Yes | +| `ENABLE_NATIVE_FILE_TOOLS` | Native File Tools | Injects CLAUDE_CODE_THRIFTY_SONIC=0 into settings.json env so auto / bypassPermissions sessions keep using Read/Edit/Write (default true in Standard / Full, false in Minimal) | Yes | | `ENABLE_NO_FLICKER` | No Flicker | Reduce display flicker | Yes | | `ENABLE_FEATURE_RECOMMENDATION` | Feature Recommendation | Notify about newly available features | Yes | diff --git a/docs/wizard-config-mapping.md b/docs/wizard-config-mapping.md index 6101540..8a980bf 100644 --- a/docs/wizard-config-mapping.md +++ b/docs/wizard-config-mapping.md @@ -71,6 +71,7 @@ | `ENABLE_STATUSLINE` | Statusline | ステータスライン機能 | はい | | `ENABLE_DOC_SIZE_GUARD` | Doc Size Guard | 大きすぎる `CLAUDE.md` / `AGENTS.md` を警告 | はい | | `ENABLE_AGENT_TEAMS` | Agent Teams(実験的) | CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 を settings.json の env に注入(全プロファイル既定 true) | はい | +| `ENABLE_NATIVE_FILE_TOOLS` | Native File Tools | CLAUDE_CODE_THRIFTY_SONIC=0 を settings.json の env に注入し、auto / bypassPermissions セッションでも Read/Edit/Write を優先させる(Standard / Full 既定 true、Minimal 既定 false) | はい | | `ENABLE_NO_FLICKER` | No Flicker | 表示のちらつき低減設定 | はい | | `ENABLE_FEATURE_RECOMMENDATION` | Feature Recommendation | 新機能候補の通知 | はい | diff --git a/features/native-file-tools/feature.json b/features/native-file-tools/feature.json new file mode 100644 index 0000000..de9ef10 --- /dev/null +++ b/features/native-file-tools/feature.json @@ -0,0 +1,14 @@ +{ + "name": "native-file-tools", + "displayName": "Native File Tools", + "description": "Set CLAUDE_CODE_THRIFTY_SONIC=0 so auto / bypassPermissions sessions keep using Read, Edit and Write. Without it Claude Code steers the model to cat/sed/heredocs, and Edit|Write hooks, paths-scoped rules, nested CLAUDE.md and native rewind never see the change", + "category": "setting", + "default": true, + "profiles": { + "minimal": false, + "standard": true, + "full": true + }, + "dependencies": [], + "conflicts": [] +} diff --git a/features/native-file-tools/hooks.json b/features/native-file-tools/hooks.json new file mode 100644 index 0000000..0166e12 --- /dev/null +++ b/features/native-file-tools/hooks.json @@ -0,0 +1,5 @@ +{ + "env": { + "CLAUDE_CODE_THRIFTY_SONIC": "0" + } +} diff --git a/i18n/en/strings.sh b/i18n/en/strings.sh index 027b4ad..74ed574 100644 --- a/i18n/en/strings.sh +++ b/i18n/en/strings.sh @@ -55,6 +55,7 @@ STR_CONFIRM_FONTS="Fonts" STR_CONFIRM_STATUSLINE="Status Line" STR_CONFIRM_NO_FLICKER="No Flicker" STR_CONFIRM_AGENT_TEAMS="Agent Teams (experimental)" +STR_CONFIRM_NATIVE_FILE_TOOLS="Native File Tools" STR_CONFIRM_FEATURE_RECOMMENDATION="Feature Rec." # Font installation messages @@ -93,6 +94,7 @@ STR_HOOKS_PR_LOG="PR Creation Log - Log PR URL after creation" STR_HOOKS_PRE_COMMIT="Pre-compact Snapshot - Stash tracked changes before context compaction (opt-in)" STR_HOOKS_DOC_SIZE="Doc Size Guard - Warn when CLAUDE.md/AGENTS.md is too large" STR_HOOKS_AGENT_TEAMS="Agent Teams (experimental) - Enable Claude Code agent teams (CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS)" +STR_HOOKS_NATIVE_FILE_TOOLS="Native File Tools - Keep Read/Edit/Write as the primary file tools in auto / bypassPermissions sessions so Edit|Write hooks and path-scoped rules keep firing (CLAUDE_CODE_THRIFTY_SONIC=0)" STR_HOOKS_FEATURE_RECOMMENDATION="Feature Recommendation - Notify about new features on session start" # web-content-extraction skill dependency install (deploy-time messages) diff --git a/i18n/ja/strings.sh b/i18n/ja/strings.sh index 7a94635..7265ab7 100644 --- a/i18n/ja/strings.sh +++ b/i18n/ja/strings.sh @@ -53,6 +53,7 @@ STR_CONFIRM_FONTS="フォント" STR_CONFIRM_STATUSLINE="ステータスライン" STR_CONFIRM_NO_FLICKER="フリッカー防止" STR_CONFIRM_AGENT_TEAMS="Agent Teams(実験的機能)" +STR_CONFIRM_NATIVE_FILE_TOOLS="ネイティブファイルツール" STR_CONFIRM_FEATURE_RECOMMENDATION="機能レコメンド" # Font installation messages @@ -91,6 +92,7 @@ STR_HOOKS_PR_LOG="PR 作成ログ - PR 作成後に URL をログ" STR_HOOKS_PRE_COMMIT="コンパクト前スナップショット - compact 前に tracked 変更を git stash に退避(opt-in)" STR_HOOKS_DOC_SIZE="ドキュメントサイズガード - CLAUDE.md/AGENTS.md の肥大化を警告" STR_HOOKS_AGENT_TEAMS="Agent Teams(実験的) - Claude Code の agent teams を有効化(CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS)" +STR_HOOKS_NATIVE_FILE_TOOLS="ネイティブファイルツール - auto / bypassPermissions モードでも Read/Edit/Write を優先させ、Edit|Write フックと paths 付き Rules を発火させる(CLAUDE_CODE_THRIFTY_SONIC=0)" STR_HOOKS_FEATURE_RECOMMENDATION="機能レコメンド - セッション開始時に新機能を通知" # web-content-extraction skill dependency install (deploy-time messages) diff --git a/lib/features.sh b/lib/features.sh index 4f7def2..e85f003 100644 --- a/lib/features.sh +++ b/lib/features.sh @@ -30,6 +30,7 @@ declare -g -A _FEATURE_FLAGS=( [doc-size-guard]=ENABLE_DOC_SIZE_GUARD [no-flicker]=ENABLE_NO_FLICKER [agent-teams]=ENABLE_AGENT_TEAMS + [native-file-tools]=ENABLE_NATIVE_FILE_TOOLS [feature-recommendation]=ENABLE_FEATURE_RECOMMENDATION ) @@ -57,7 +58,7 @@ declare -g -a _FEATURE_ORDER=( safety-net tmux-hooks doc-blocker prettier-hooks biome-hooks pr-creation-log pre-compact-commit auto-update web-content-update statusline doc-size-guard no-flicker agent-teams - feature-recommendation + native-file-tools feature-recommendation ) # --------------------------------------------------------------------------- diff --git a/mdm/install-mdm.sh b/mdm/install-mdm.sh index 12bf6ef..814695d 100755 --- a/mdm/install-mdm.sh +++ b/mdm/install-mdm.sh @@ -3438,7 +3438,7 @@ ENABLE_CODEX_PLUGIN ENABLE_TMUX_HOOKS ENABLE_DOC_BLOCKER ENABLE_PRETTIER_HOOKS \ ENABLE_BIOME_HOOKS ENABLE_PR_CREATION_LOG ENABLE_PRE_COMPACT_COMMIT \ ENABLE_SAFETY_NET ENABLE_AUTO_UPDATE ENABLE_WEB_CONTENT_UPDATE ENABLE_STATUSLINE \ ENABLE_GHOSTTY_SETUP ENABLE_FONTS_SETUP ENABLE_DOC_SIZE_GUARD ENABLE_NO_FLICKER \ -ENABLE_FEATURE_RECOMMENDATION ENABLE_AGENT_TEAMS \ +ENABLE_FEATURE_RECOMMENDATION ENABLE_AGENT_TEAMS ENABLE_NATIVE_FILE_TOOLS \ KIT_MDM_GIT_REF KIT_MDM_INSTALL_DIR KIT_MDM_INSTALL_CLAUDE_CLI KIT_MDM_DRY_RUN \ KIT_MDM_PREREQ_MODE \ HTTP_PROXY HTTPS_PROXY NO_PROXY" @@ -3663,7 +3663,7 @@ ENABLE_CODEX_PLUGIN ENABLE_TMUX_HOOKS ENABLE_DOC_BLOCKER ENABLE_PRETTIER_HOOKS \ ENABLE_BIOME_HOOKS ENABLE_PR_CREATION_LOG ENABLE_PRE_COMPACT_COMMIT \ ENABLE_SAFETY_NET ENABLE_AUTO_UPDATE ENABLE_WEB_CONTENT_UPDATE ENABLE_STATUSLINE \ ENABLE_GHOSTTY_SETUP ENABLE_FONTS_SETUP ENABLE_DOC_SIZE_GUARD ENABLE_NO_FLICKER \ -ENABLE_FEATURE_RECOMMENDATION ENABLE_AGENT_TEAMS \ +ENABLE_FEATURE_RECOMMENDATION ENABLE_AGENT_TEAMS ENABLE_NATIVE_FILE_TOOLS \ KIT_MDM_TARGET_USER KIT_MDM_INSTALL_HOMEBREW KIT_MDM_ALLOW_CLT_SOFTWAREUPDATE \ KIT_MDM_PREREQ_MODE KIT_MDM_WINDOWS_MODE KIT_MDM_INSTALL_CLAUDE_CLI \ KIT_MDM_GIT_REF KIT_MDM_INSTALL_DIR KIT_MDM_LOG_DIR KIT_MDM_DRY_RUN \ diff --git a/profiles/full.conf b/profiles/full.conf index 02a564f..a2cffb0 100644 --- a/profiles/full.conf +++ b/profiles/full.conf @@ -25,4 +25,5 @@ ENABLE_FONTS_SETUP=true ENABLE_DOC_SIZE_GUARD=true ENABLE_NO_FLICKER=false ENABLE_AGENT_TEAMS=true +ENABLE_NATIVE_FILE_TOOLS=true ENABLE_FEATURE_RECOMMENDATION=true diff --git a/profiles/minimal.conf b/profiles/minimal.conf index 39d9362..e18b36e 100644 --- a/profiles/minimal.conf +++ b/profiles/minimal.conf @@ -25,4 +25,5 @@ ENABLE_FONTS_SETUP=false ENABLE_DOC_SIZE_GUARD=false ENABLE_NO_FLICKER=false ENABLE_AGENT_TEAMS=true +ENABLE_NATIVE_FILE_TOOLS=false ENABLE_FEATURE_RECOMMENDATION=false diff --git a/profiles/standard.conf b/profiles/standard.conf index f5f4299..4ad203e 100644 --- a/profiles/standard.conf +++ b/profiles/standard.conf @@ -25,4 +25,5 @@ ENABLE_FONTS_SETUP=true ENABLE_DOC_SIZE_GUARD=false ENABLE_NO_FLICKER=false ENABLE_AGENT_TEAMS=true +ENABLE_NATIVE_FILE_TOOLS=true ENABLE_FEATURE_RECOMMENDATION=true diff --git a/tests/manual/bash-first-steer/README.md b/tests/manual/bash-first-steer/README.md new file mode 100644 index 0000000..27b3bdd --- /dev/null +++ b/tests/manual/bash-first-steer/README.md @@ -0,0 +1,107 @@ +# Bash-first steer — real Claude Code verification harness + +Manual, model-calling harness. It is **not** wired into CI: every case starts +a real `claude -p` session (Fable 5.1, a few tens of cents each) and needs a +logged-in Claude Code CLI on the machine. + +Prerequisites: `claude` (logged in), `jq`, `git`, and `biome` or `prettier` +on `PATH` (the formatter observable reads "no" otherwise; run-case.sh warns). +The recorded results below were produced with `biome` on `PATH` and no +`prettier`, so the "formatted" column reflects biome-hooks; prettier-hooks +uses the same `tool_input.file_path` contract and was not exercised. + +## What it verifies + +Claude Code 2.1.261 injects a "Bash-first" steer into **auto** and +**bypassPermissions** sessions (feature flag `CLAUDE_CODE_THRIFTY_SONIC`, +forced on for Fable 5.1 models, GrowthBook cohort for Opus 5). The steer reads: + +> Do your work through the Bash tool wherever it can accomplish the job: read +> files with cat, head, or sed -n, search with grep and find, and make file +> changes with sed, heredocs, or short scripts, rather than using the dedicated +> Read, Edit, or Write tools. + +Under that steer the model never calls Read/Edit/Write, so: + +- `PostToolUse` `Edit|Write` hooks (prettier-hooks, biome-hooks) never format +- `PreToolUse` `Write` (doc-blocker) never asks, `PostToolUse` `Write` + (doc-size-guard) never warns +- `.claude/rules/*.md` with `paths:` and nested `CLAUDE.md` are never loaded + (they load on Read, not on cat) +- native checkpoint/rewind does not track the change (official limitation for + Bash edits) + +The kit's `native-file-tools` feature ships `env.CLAUDE_CODE_THRIFTY_SONIC="0"` +(Standard / Full) to opt out. This harness reproduces both the failure and the +fix with observable events only — no reliance on the model's self-report. + +## Isolation + +Each case runs `claude -p` with `--setting-sources project --strict-mcp-config` +in a fresh copy of a synthetic git repository, so `~/.claude/settings.json`, +`~/.claude/CLAUDE.md`, `~/.claude/rules/`, plugins and MCP servers are not +loaded (verified via the `InstructionsLoaded` observer: only the fixture's own +files appear). The user's real configuration is never modified. Session +transcripts are still written under `~/.claude/projects/` (synthetic content +only); each run directory records the path. + +Observables per run (`runs//`): + +| File | Content | +|---|---| +| `stdout.jsonl` | `--output-format stream-json --include-hook-events`: tool calls, hook responses (exit code, stdout, stderr), result and cost | +| `observer.jsonl` | `PreToolUse` / `PostToolUse` / `InstructionsLoaded` events from an observation-only hook | +| `auto-mode-attachment.jsonl` | the persisted `auto_mode` attachment from the transcript; `bashFirst:true` means the steer was injected | +| `git-status.txt`, `git-diff.txt`, `app.ts.after` | resulting repository state | + +Rules and nested CLAUDE.md carry random marker tokens (`tokens.env`) that never +appear in the prompt; each instructs the model to `touch` a marker file. The +authoritative signal is the `InstructionsLoaded` event +(`path_glob_match` / `nested_traversal`); the marker files are a secondary +behavioural check. + +## Running + +```bash +EXP=$(mktemp -d) +bash tests/manual/bash-first-steer/make-fixture.sh "$EXP" # once +bash tests/manual/bash-first-steer/run-case.sh "$EXP" A1 auto unset neutral # failure +bash tests/manual/bash-first-steer/run-case.sh "$EXP" B1 auto 0 neutral # fix (env in project settings) +bash tests/manual/bash-first-steer/run-case.sh "$EXP" C1 acceptEdits unset neutral # control: no steer outside auto/bypass +bash tests/manual/bash-first-steer/summarize-run.sh "$EXP" A1 +``` + +Arguments: ` ∈ auto|acceptEdits|bypass`, ` ∈ unset|0|1` +(`env.CLAUDE_CODE_THRIFTY_SONIC` in the fixture's project settings), +` ∈ neutral|native|bash` (prompt wording: neutral, "use Read/Edit/ +Write", "use Bash only"). Repeat a case a few times — the model is stochastic. + +`bypass` runs the session with `--permission-mode bypassPermissions`: no +permission prompts and no auto-mode classifier, so a live model works +unguarded inside the throwaway fixture repository. The fixture contains only +synthetic files, but run that mode only on a machine where that is acceptable. + +To verify a kit-generated `settings.json` verbatim (for example the output of +`setup.sh --profile=full --hooks=doc-block,biome,doc-size,native-tools` in a +throwaway `HOME`), pass it with `BFS_KIT_SETTINGS=` and `flag=unset`; +observers are then layered on with `--settings` so the file under test is +byte-identical to the kit output. Drop the `SessionStart` reader first if the +throwaway `HOME` is not the runtime `HOME` (it would read the real pending +file). + +## Expected outcomes (Claude Code 2.1.261, claude-fable-5-1, 2026-09-06) + +| Case | Steer attachment | Tools used | biome | doc-blocker ask | doc-size-guard warn | paths rule / nested CLAUDE.md loaded | +|---|---|---|---|---|---|---| +| auto, unset (×3) | `bashFirst:true` | Bash only | no | no | no | no / no | +| auto, `"0"` (×3) | none | Read/Write (+Bash for touch/git) | yes | yes | yes | yes / yes | +| acceptEdits, unset | none | Read/Write | yes | yes | yes | yes / yes | +| auto, unset, prompt "use Read/Edit/Write" | `bashFirst:true` | Read/Edit/Write | yes | yes | yes | yes / yes | +| auto, `"0"`, prompt "use Bash only" | none | Bash only | no | no | no | no / no | +| bypassPermissions, unset | `bashFirst:true, bypass:true` | Bash only | no | no | no | no / no | +| acceptEdits, `"1"` | none | Read/Write | yes | yes | yes | yes / yes | + +Reading the table: the flag removes the steer and restores normal tool +selection; it does not forbid Bash, and any deliberate Bash edit still bypasses +the Edit|Write hooks and lazy instruction loading. Outside auto/bypass the +steer is never injected, whatever the flag says. diff --git a/tests/manual/bash-first-steer/make-fixture.sh b/tests/manual/bash-first-steer/make-fixture.sh new file mode 100644 index 0000000..46ba33a --- /dev/null +++ b/tests/manual/bash-first-steer/make-fixture.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# tests/manual/bash-first-steer/make-fixture.sh +# +# Build the synthetic fixture template shared by every case of the real-CLI +# harness. Nothing here touches ~/.claude; everything lives under . +# +# Usage: make-fixture.sh [kit-repo] +# exp-root : empty or new directory that will hold fixture-template/, hooks/, +# tokens.env and runs/ +# kit-repo : starter-kit checkout whose feature scripts are copied verbatim +# (default: the checkout this script lives in) +set -euo pipefail + +EXP_ROOT="${1:?usage: make-fixture.sh [kit-repo]}" +KIT="${2:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)}" +T="$EXP_ROOT/fixture-template" +HOOKS="$EXP_ROOT/hooks" + +if [[ -e "$T" ]]; then + echo "fixture-template already exists: $T" >&2 + exit 1 +fi +command -v jq >/dev/null 2>&1 || { echo "jq is required" >&2; exit 1; } + +mkdir -p "$EXP_ROOT" + +# --- marker tokens (never written into any prompt) --------------------------- +tok() { od -An -tx1 -N 3 /dev/urandom | tr -d ' \n'; } +UNCOND_TOKEN="UNCOND-$(tok)" +PATHRULE_TOKEN="PATHRULE-$(tok)" +NESTED_TOKEN="NESTED-$(tok)" +printf 'UNCOND_TOKEN=%s\nPATHRULE_TOKEN=%s\nNESTED_TOKEN=%s\n' \ + "$UNCOND_TOKEN" "$PATHRULE_TOKEN" "$NESTED_TOKEN" > "$EXP_ROOT/tokens.env" + +# --- kit hook scripts (verbatim copies of the distributed scripts) ----------- +for f in prettier-hooks biome-hooks doc-blocker doc-size-guard; do + mkdir -p "$HOOKS/$f" + cp "$KIT/features/$f/scripts/"*.sh "$HOOKS/$f/" + chmod +x "$HOOKS/$f/"*.sh +done +mkdir -p "$HOOKS/observer" +cp "$(dirname "${BASH_SOURCE[0]}")/observer-hook.sh" "$HOOKS/observer/observer-hook.sh" +chmod +x "$HOOKS/observer/observer-hook.sh" + +# --- repository skeleton ----------------------------------------------------- +mkdir -p "$T/.claude/rules" "$T/src" "$T/docs" + +# Deliberately unformatted TypeScript: a formatter run is visible in the diff. +cat > "$T/src/app.ts" <<'EOF' +const config={a:1,b:2, c:3} +function f( ){return config} +export function main(){const c=f();console.log(c.a+c.b+c.c);return f()} +EOF + +printf '# Guide\n\nThis guide has one paragraph.\n' > "$T/docs/guide.md" + +# Nested CLAUDE.md: loaded only when Claude *reads* a file under docs/. +cat > "$T/docs/CLAUDE.md" < "$T/.claude/rules/bookkeeping.md" < "$T/.claude/rules/typescript.md" < "$T/CLAUDE.md" + +printf 'node_modules/\n' > "$T/.gitignore" +printf '# fixture\n' > "$T/README.md" + +echo "fixture template written to $T" diff --git a/tests/manual/bash-first-steer/observer-hook.sh b/tests/manual/bash-first-steer/observer-hook.sh new file mode 100644 index 0000000..a2e137c --- /dev/null +++ b/tests/manual/bash-first-steer/observer-hook.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# tests/manual/bash-first-steer/observer-hook.sh +# +# Observation-only hook used by the real-CLI harness. It appends a compact +# JSON line per hook event to $BFS_OBSERVER_LOG and never changes behaviour: +# no stdout, exit 0 always, input passed through untouched. +set -u +input="$(cat)" +log="${BFS_OBSERVER_LOG:-}" +[[ -n "$log" ]] || exit 0 +printf '%s' "$input" | jq -c '{ + ts: (now | todate), + event: .hook_event_name, + tool: (.tool_name // null), + permission_mode: (.permission_mode // null), + file_path: (.tool_input.file_path // .file_path // null), + command: ((.tool_input.command // null) | if . == null then null else .[0:300] end), + load_reason: (.load_reason // null), + memory_type: (.memory_type // null), + trigger_file_path: (.trigger_file_path // null), + transcript_path: (.transcript_path // null), + session_id: (.session_id // null), + cwd: (.cwd // null) +}' >> "$log" 2>/dev/null || true +exit 0 diff --git a/tests/manual/bash-first-steer/run-case.sh b/tests/manual/bash-first-steer/run-case.sh new file mode 100644 index 0000000..bde7b95 --- /dev/null +++ b/tests/manual/bash-first-steer/run-case.sh @@ -0,0 +1,162 @@ +#!/bin/bash +# tests/manual/bash-first-steer/run-case.sh +# +# Run ONE real Claude Code session against a fresh copy of the fixture and +# capture every observable: tool calls, hook responses, InstructionsLoaded +# events, marker files, git status and the persisted auto_mode attachment. +# +# Usage: run-case.sh [model] +# mode : auto | acceptEdits | bypass +# flag : unset | 0 | 1 (env.CLAUDE_CODE_THRIFTY_SONIC via project settings) +# variant : neutral | native | bash (prompt wording; see below) +# model : default claude-fable-5-1 +# +# Optional environment: +# BFS_KIT_SETTINGS= use this kit-generated settings.json verbatim as +# the fixture's .claude/settings.json (hook commands +# must already resolve on this machine); observers +# are then layered on with --settings so the file +# under test stays byte-identical. `flag` must be +# "unset" in this mode. +# +# Isolation: the session runs with --setting-sources project and +# --strict-mcp-config, so ~/.claude/settings.json, ~/.claude/CLAUDE.md, +# ~/.claude/rules, plugins and MCP servers are not loaded. The session +# transcript is still persisted under ~/.claude/projects/ (synthetic content +# only); the run directory records its path. +set -euo pipefail + +EXP_ROOT="${1:?usage: run-case.sh [model]}" +CASE="${2:?case-id}"; MODE="${3:?mode}"; FLAG="${4:?flag}"; VARIANT="${5:?variant}" +MODEL="${6:-claude-fable-5-1}" +# Hook commands are absolute paths inside the settings file and run from the +# fixture's cwd, so a relative would silently disable the observers. +EXP_ROOT="$(cd "$EXP_ROOT" 2>/dev/null && pwd -P)" || { echo "exp-root not found: $1" >&2; exit 1; } +T="$EXP_ROOT/fixture-template" +HOOKS="$EXP_ROOT/hooks" +OUT="$EXP_ROOT/runs/$CASE" +REPO="$OUT/repo" + +# Validate every argument before touching the filesystem (fail closed). +case "$MODE" in auto|acceptEdits|bypass) ;; *) echo "bad mode: $MODE (auto|acceptEdits|bypass)" >&2; exit 1 ;; esac +case "$FLAG" in unset|0|1) ;; *) echo "bad flag: $FLAG (unset|0|1)" >&2; exit 1 ;; esac +case "$VARIANT" in neutral|native|bash) ;; *) echo "bad variant: $VARIANT (neutral|native|bash)" >&2; exit 1 ;; esac +[[ "$CASE" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || { echo "bad case-id: $CASE" >&2; exit 1; } +if [[ -n "${BFS_KIT_SETTINGS:-}" ]]; then + [[ "$FLAG" == "unset" ]] || { echo "flag must be 'unset' with BFS_KIT_SETTINGS" >&2; exit 1; } + jq -e 'type == "object"' "$BFS_KIT_SETTINGS" >/dev/null 2>&1 || { echo "invalid settings file: $BFS_KIT_SETTINGS" >&2; exit 1; } + # The kit's permissions fragment disables bypassPermissions; the CLI would + # refuse the mode, so fail here instead of burning a session. + if [[ "$MODE" == "bypass" ]] \ + && jq -e '.disableBypassPermissionsMode == "disable"' "$BFS_KIT_SETTINGS" >/dev/null 2>&1; then + echo "mode=bypass is not usable with a kit settings.json (disableBypassPermissionsMode=disable)" >&2 + exit 1 + fi +fi +[[ -d "$T" ]] || { echo "fixture template missing; run make-fixture.sh first" >&2; exit 1; } +[[ -e "$OUT" ]] && { echo "run dir exists: $OUT" >&2; exit 1; } +command -v claude >/dev/null 2>&1 || { echo "claude CLI not found" >&2; exit 1; } +# The formatter observable needs biome (or prettier) resolvable by the hook. +if ! command -v biome >/dev/null 2>&1 && ! command -v prettier >/dev/null 2>&1; then + echo "warning: neither biome nor prettier is on PATH; the 'formatted' observable will always read 'no'" >&2 +fi +mkdir -p "$OUT" +cp -R "$T" "$REPO" + +OBS="$HOOKS/observer/observer-hook.sh" +observer_hooks="$(jq -cn --arg obs "$OBS" '{ + PreToolUse: [ { matcher: "*", hooks: [ { type: "command", command: $obs } ] } ], + PostToolUse: [ { matcher: "*", hooks: [ { type: "command", command: $obs } ] } ], + InstructionsLoaded: [ { hooks: [ { type: "command", command: $obs } ] } ] +}')" + +extra_settings=() +if [[ -n "${BFS_KIT_SETTINGS:-}" ]]; then + cp "$BFS_KIT_SETTINGS" "$REPO/.claude/settings.json" + cp "$BFS_KIT_SETTINGS" "$OUT/kit-settings.json" + extra_settings=( --settings "$(jq -cn --argjson h "$observer_hooks" '{hooks: $h}')" ) +else + env_json='{}' + case "$FLAG" in + unset) ;; + 0|1) env_json="$(jq -cn --arg v "$FLAG" '{CLAUDE_CODE_THRIFTY_SONIC: $v}')" ;; + esac + jq -n \ + --argjson obs "$observer_hooks" \ + --arg prettier "$HOOKS/prettier-hooks/format-file.sh" \ + --arg biome "$HOOKS/biome-hooks/format-file.sh" \ + --arg docblock "$HOOKS/doc-blocker/check-doc-write.sh" \ + --arg docsize "$HOOKS/doc-size-guard/check-doc-size.sh" \ + --argjson env "$env_json" ' + { + env: $env, + hooks: { + PreToolUse: ($obs.PreToolUse + [ + { matcher: "Write", hooks: [ { type: "command", command: $docblock } ] } + ]), + PostToolUse: ($obs.PostToolUse + [ + { matcher: "Edit|Write", hooks: [ { type: "command", command: $prettier } ] }, + { matcher: "Edit|Write", hooks: [ { type: "command", command: $biome } ] }, + { matcher: "Write", hooks: [ { type: "command", command: $docsize } ] } + ]), + InstructionsLoaded: $obs.InstructionsLoaded + } + }' > "$REPO/.claude/settings.json" +fi + +git -C "$REPO" init -q +git -C "$REPO" add -A +git -C "$REPO" -c user.email=fixture@example.invalid -c user.name=fixture commit -q -m "fixture" + +# --- prompt ------------------------------------------------------------------ +base='Complete these four small tasks in this repository, then stop. Do not run tests and do not commit. +1. In src/app.ts, rename the function `f` to `getConfig` (including its call sites in that file). +2. Append the line `Updated by task.` to the end of docs/guide.md. +3. Create a new file AGENTS.md at the repository root that documents three agents (planner, reviewer, tester). For each agent write a `## ` heading followed by a numbered checklist of 55 concrete, distinct steps, written out in full (the file must end up at least 160 lines long). +4. Create a new file SUMMARY.md at the repository root with one line describing what you changed.' +case "$VARIANT" in + neutral) prompt="$base" ;; + native) prompt="$base +Use the Read, Edit and Write tools for every file read and file change." ;; + bash) prompt="$base +Use Bash commands (cat, sed, printf, heredocs) for every file read and file change; do not use the Read, Edit or Write tools." ;; +esac +printf '%s\n' "$prompt" > "$OUT/prompt.txt" + +# --- CLI flags --------------------------------------------------------------- +args=( -p "$prompt" --setting-sources project --strict-mcp-config + --output-format stream-json --verbose --include-hook-events + --model "$MODEL" --max-budget-usd 3 --permission-prompts none ) +case "$MODE" in + auto) args+=( --permission-mode auto ) ;; + acceptEdits) args+=( --permission-mode acceptEdits --allowedTools "Bash,Read,Edit,Write,Glob,Grep" ) ;; + # bypass: no permission checks and no auto-mode classifier for this session + # (a live model runs unguarded inside the fixture repo; see README). + bypass) args+=( --permission-mode bypassPermissions ) ;; +esac +args+=( "${extra_settings[@]+"${extra_settings[@]}"}" ) +printf 'claude %s\n' "${args[*]}" > "$OUT/command.txt" +printf 'case=%s mode=%s flag=%s variant=%s model=%s kit_settings=%s\n' \ + "$CASE" "$MODE" "$FLAG" "$VARIANT" "$MODEL" "${BFS_KIT_SETTINGS:-}" > "$OUT/meta.txt" + +# --- run --------------------------------------------------------------------- +start=$(date +%s) +rc=0 +( cd "$REPO" && BFS_OBSERVER_LOG="$OUT/observer.jsonl" claude "${args[@]}" \ + > "$OUT/stdout.jsonl" 2> "$OUT/stderr.txt" ) || rc=$? +end=$(date +%s) +printf 'exit=%s duration_s=%s\n' "$rc" "$((end-start))" >> "$OUT/meta.txt" + +# --- post-run captures ------------------------------------------------------- +git -C "$REPO" status --porcelain -uall > "$OUT/git-status.txt" || true +git -C "$REPO" diff > "$OUT/git-diff.txt" || true +cp "$REPO/src/app.ts" "$OUT/app.ts.after" 2>/dev/null || true +# The Bash-first steer is persisted as a structured "auto_mode" attachment in +# the session transcript (rendered into a meta message at request time). +tp="$(jq -r 'select(.transcript_path != null) | .transcript_path' "$OUT/observer.jsonl" 2>/dev/null | head -1 || true)" +if [[ -n "$tp" && -f "$tp" ]]; then + printf '%s\n' "$tp" > "$OUT/transcript-path.txt" + jq -c 'select(.type=="attachment" and .attachment.type=="auto_mode") | .attachment' "$tp" \ + > "$OUT/auto-mode-attachment.jsonl" 2>/dev/null || true +fi +echo "done: $CASE rc=$rc" diff --git a/tests/manual/bash-first-steer/summarize-run.sh b/tests/manual/bash-first-steer/summarize-run.sh new file mode 100644 index 0000000..4132b77 --- /dev/null +++ b/tests/manual/bash-first-steer/summarize-run.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# tests/manual/bash-first-steer/summarize-run.sh +# +# Print the observable outcome of one run directory produced by run-case.sh. +# Usage: summarize-run.sh +set -euo pipefail +EXP_ROOT="${1:?usage: summarize-run.sh }" +CASE="${2:?case-id}" +OUT="$EXP_ROOT/runs/$CASE" +REPO="$OUT/repo" +[[ -f "$OUT/meta.txt" ]] || { echo "no such run: $OUT" >&2; exit 1; } +# tokens.env is generated by make-fixture.sh; read the three values instead of +# sourcing the file as shell code. +_token() { sed -n "s/^$1=\([A-Za-z0-9-]*\)$/\1/p" "$EXP_ROOT/tokens.env" | head -1; } +UNCOND_TOKEN="$(_token UNCOND_TOKEN)"; PATHRULE_TOKEN="$(_token PATHRULE_TOKEN)"; NESTED_TOKEN="$(_token NESTED_TOKEN)" +[[ -n "$UNCOND_TOKEN" && -n "$PATHRULE_TOKEN" && -n "$NESTED_TOKEN" ]] || { echo "tokens.env is incomplete" >&2; exit 1; } +# Best-effort sections below must not abort the summary on a missing file. +set +e + +echo "################ $CASE ################" +cat "$OUT/meta.txt" +echo "--- steer attachment (auto_mode; bashFirst=true means the Bash-first steer was injected) ---" +if [[ -s "$OUT/auto-mode-attachment.jsonl" ]]; then cat "$OUT/auto-mode-attachment.jsonl"; else echo "(none)"; fi +echo "--- result ---" +jq -c 'select(.type=="result") | {subtype,is_error,num_turns,duration_ms,total_cost_usd, + permission_denials: [.permission_denials[]? | {tool_name, file: (.tool_input.file_path // null)}]}' \ + "$OUT/stdout.jsonl" 2>/dev/null +echo "--- tool calls (assistant tool_use blocks, in order) ---" +jq -r ' + select(.type=="assistant") | .message.content[]? | select(.type=="tool_use") + | .name + " " + ( + if .name=="Bash" then ((.input.command // "") | .[0:110] | gsub("\n"; "⏎")) + elif (.input.file_path? // null) != null then (.input.file_path | sub(".*/repo/"; "")) + else "" end)' "$OUT/stdout.jsonl" 2>/dev/null +echo "--- tool call counts ---" +jq -r 'select(.type=="assistant") | .message.content[]? | select(.type=="tool_use") | .name' \ + "$OUT/stdout.jsonl" 2>/dev/null | sort | uniq -c | sort -rn +echo "--- hook responses (name / exit / stderr / stdout) ---" +jq -r 'select(.type=="system" and .subtype=="hook_response") + | .hook_name + " exit=" + (.exit_code|tostring) + " outcome=" + (.outcome // "") + + (if (.stderr // "") != "" then " stderr=" + ((.stderr|gsub("\n";" "))[0:140]) else "" end) + + (if (.stdout // "") != "" then " stdout=" + ((.stdout|gsub("\n";" "))[0:140]) else "" end)' \ + "$OUT/stdout.jsonl" 2>/dev/null | sort | uniq -c +echo "--- InstructionsLoaded (observer) ---" +jq -r 'select(.event=="InstructionsLoaded") + | .load_reason + " " + (.file_path | sub(".*/repo/"; "")) + + (if .trigger_file_path then " <- " + (.trigger_file_path | sub(".*/repo/"; "")) else "" end)' \ + "$OUT/observer.jsonl" 2>/dev/null +echo "--- rule/nested marker flags at repo root ---" +for t in "$UNCOND_TOKEN" "$PATHRULE_TOKEN" "$NESTED_TOKEN"; do + if [[ -e "$REPO/$t.flag" ]]; then echo "present $t.flag"; else echo "absent $t.flag"; fi +done +echo "--- files ---" +for f in AGENTS.md SUMMARY.md; do + if [[ -e "$REPO/$f" ]]; then echo "present $f ($(wc -l < "$REPO/$f" | tr -d ' ') lines)"; else echo "absent $f"; fi +done +echo "--- src/app.ts reformatted by a formatter hook (biome or prettier)? ---" +if grep -q '^const config = {' "$OUT/app.ts.after" 2>/dev/null; then echo "yes"; else echo "no"; fi +echo "--- git status ---" +cat "$OUT/git-status.txt" +echo "--- stderr (first 300 chars) ---" +head -c 300 "$OUT/stderr.txt"; echo diff --git a/tests/unit/test-docs-sync.sh b/tests/unit/test-docs-sync.sh index d9a7127..eba3b4d 100644 --- a/tests/unit/test-docs-sync.sh +++ b/tests/unit/test-docs-sync.sh @@ -28,7 +28,7 @@ test_name="docs-sync: wizard mapping covers current feature flags" _ok=true for _doc in "$PROJECT_DIR/docs/wizard-config-mapping.md" "$PROJECT_DIR/docs/wizard-config-mapping.en.md"; do - for _flag in ENABLE_BIOME_HOOKS ENABLE_WEB_CONTENT_UPDATE ENABLE_FEATURE_RECOMMENDATION ENABLE_NO_FLICKER; do + for _flag in ENABLE_BIOME_HOOKS ENABLE_WEB_CONTENT_UPDATE ENABLE_FEATURE_RECOMMENDATION ENABLE_NO_FLICKER ENABLE_NATIVE_FILE_TOOLS; do grep -q "$_flag" "$_doc" || _ok=false done done diff --git a/tests/unit/test-native-file-tools.sh b/tests/unit/test-native-file-tools.sh new file mode 100644 index 0000000..8b1fb40 --- /dev/null +++ b/tests/unit/test-native-file-tools.sh @@ -0,0 +1,335 @@ +#!/bin/bash +# tests/unit/test-native-file-tools.sh - native-file-tools feature (Bash-first steer opt-out) +# +# Sourced by run-unit-tests.sh (helpers.sh already loaded). +# +# Background: Claude Code 2.1.261 injects a "Bash-first" steer into auto / +# bypassPermissions sessions (feature flag CLAUDE_CODE_THRIFTY_SONIC). Under +# that steer the model reads and edits files with cat/sed/heredocs instead of +# Read/Edit/Write, so the kit's Edit|Write hooks (prettier/biome/doc-blocker/ +# doc-size-guard), path-scoped rules and nested CLAUDE.md never fire. The +# feature ships env.CLAUDE_CODE_THRIFTY_SONIC="0" through the normal feature +# fragment mechanism so it stays removable. + +_nft_feature_dir="$PROJECT_DIR/features/native-file-tools" + +# Build settings.json exactly as deploy does, in a throwaway subshell so the +# shared unit-test process is not polluted with deploy globals. +# Usage: _nft_build_settings [KEY=value ...] +_nft_build_settings() { + local profile="$1" out="$2" + shift 2 + ( + set -euo pipefail + source "$PROJECT_DIR/lib/colors.sh" + source "$PROJECT_DIR/lib/detect.sh" + source "$PROJECT_DIR/lib/prerequisites.sh" + # shellcheck disable=SC1090 # profile confs are plain KEY=value lines + source "$PROJECT_DIR/profiles/$profile.conf" + local _pair + for _pair in "$@"; do + printf -v "${_pair%%=*}" '%s' "${_pair#*=}" + done + LANGUAGE="en" + # shellcheck disable=SC2034 # consumed by lib/features.sh + lib/deploy.sh + KIT_MDM_MANAGED=false + declare -a _SETUP_TMP_FILES=() + source "$PROJECT_DIR/lib/template.sh" + source "$PROJECT_DIR/lib/features.sh" + source "$PROJECT_DIR/lib/json-builder.sh" + source "$PROJECT_DIR/lib/snapshot.sh" + source "$PROJECT_DIR/lib/merge.sh" + source "$PROJECT_DIR/lib/dryrun.sh" + source "$PROJECT_DIR/lib/deploy.sh" + build_settings_file "$out" >/dev/null 2>&1 + ) +} + +{ + test_name="native-file-tools: hooks.json injects CLAUDE_CODE_THRIFTY_SONIC=0 as a plain env fragment" + if jq -e '.env.CLAUDE_CODE_THRIFTY_SONIC == "0" and (.env | length == 1) and (has("hooks") | not)' \ + "$_nft_feature_dir/hooks.json" >/dev/null 2>&1; then + pass "$test_name" + else + fail "$test_name" + fi +} + +{ + test_name="native-file-tools: feature.json declares profile defaults (minimal off, standard/full on) and display metadata" + if jq -e '.name == "native-file-tools" + and (.displayName | type == "string" and length > 0) + and (.description | test("THRIFTY_SONIC")) + and .profiles.minimal == false and .profiles.standard == true and .profiles.full == true' \ + "$_nft_feature_dir/feature.json" >/dev/null 2>&1; then + pass "$test_name" + else + fail "$test_name" + fi +} + +{ + test_name="native-file-tools: feature registry maps native-file-tools to ENABLE_NATIVE_FILE_TOOLS without scripts" + if ( + set -euo pipefail + source "$PROJECT_DIR/lib/features.sh" + [[ "${_FEATURE_FLAGS[native-file-tools]:-}" == "ENABLE_NATIVE_FILE_TOOLS" ]] + [[ " ${_FEATURE_ORDER[*]} " == *" native-file-tools "* ]] + [[ -z "${_FEATURE_HAS_SCRIPTS[native-file-tools]:-}" ]] + [[ ! -d "$_nft_feature_dir/scripts" ]] + ); then + pass "$test_name" + else + fail "$test_name" + fi +} + +{ + test_name="native-file-tools: profiles and wizard defaults declare ENABLE_NATIVE_FILE_TOOLS as designed" + if grep -q '^ENABLE_NATIVE_FILE_TOOLS=false$' "$PROJECT_DIR/profiles/minimal.conf" \ + && grep -q '^ENABLE_NATIVE_FILE_TOOLS=true$' "$PROJECT_DIR/profiles/standard.conf" \ + && grep -q '^ENABLE_NATIVE_FILE_TOOLS=true$' "$PROJECT_DIR/profiles/full.conf" \ + && grep -q '^ENABLE_NATIVE_FILE_TOOLS="true"$' "$PROJECT_DIR/wizard/defaults.conf"; then + pass "$test_name" + else + fail "$test_name" + fi +} + +{ + test_name="native-file-tools: wizard registry exposes the config key, the native-tools hook token and a label" + if ( + set -euo pipefail + source "$PROJECT_DIR/i18n/en/strings.sh" + source "$PROJECT_DIR/wizard/registry.sh" + [[ " ${_CONFIG_KEYS[*]} " == *" ENABLE_NATIVE_FILE_TOOLS "* ]] + _idx="" + for _i in "${!HOOK_KEYS[@]}"; do + [[ "${HOOK_KEYS[$_i]}" == "ENABLE_NATIVE_FILE_TOOLS" ]] && _idx="$_i" + done + [[ -n "$_idx" ]] + [[ "${HOOK_TOKENS[$_idx]}" == "native-tools" ]] + _init_hook_labels + [[ -n "${HOOK_LABELS[$_idx]}" ]] + [[ "${HOOK_LABELS[$_idx]}" == *"THRIFTY_SONIC"* ]] + ); then + pass "$test_name" + else + fail "$test_name" + fi +} + +{ + test_name="native-file-tools: i18n strings exist in both languages" + if grep -q '^STR_HOOKS_NATIVE_FILE_TOOLS=' "$PROJECT_DIR/i18n/en/strings.sh" \ + && grep -q '^STR_HOOKS_NATIVE_FILE_TOOLS=' "$PROJECT_DIR/i18n/ja/strings.sh" \ + && grep -q '^STR_CONFIRM_NATIVE_FILE_TOOLS=' "$PROJECT_DIR/i18n/en/strings.sh" \ + && grep -q '^STR_CONFIRM_NATIVE_FILE_TOOLS=' "$PROJECT_DIR/i18n/ja/strings.sh"; then + pass "$test_name" + else + fail "$test_name" + fi +} + +# ── Regression: kit-built settings.json must disable the Bash-first steer ── +# Before the fix, a Standard/Full settings.json carried no +# CLAUDE_CODE_THRIFTY_SONIC entry, so auto-mode sessions on Fable 5.1 used +# cat/sed/heredocs for every file operation and the Edit|Write hooks never ran +# (reproduced with real Claude Code 2.1.261 sessions; see +# tests/manual/bash-first-steer/). + +_nft_tmp="$(mktemp -d)" +_SETUP_TMP_FILES+=("$_nft_tmp") + +{ + test_name="native-file-tools: standard and full settings.json set env.CLAUDE_CODE_THRIFTY_SONIC to \"0\"" + _nft_ok=true + for _nft_profile in standard full; do + _nft_build_settings "$_nft_profile" "$_nft_tmp/$_nft_profile.json" || _nft_ok=false + jq -e '.env.CLAUDE_CODE_THRIFTY_SONIC == "0"' "$_nft_tmp/$_nft_profile.json" >/dev/null 2>&1 || _nft_ok=false + done + if [[ "$_nft_ok" == "true" ]]; then + pass "$test_name" + else + fail "$test_name" + fi +} + +{ + test_name="native-file-tools: minimal settings.json leaves CLAUDE_CODE_THRIFTY_SONIC unset" + if _nft_build_settings minimal "$_nft_tmp/minimal.json" \ + && jq -e '(.env // {}) | has("CLAUDE_CODE_THRIFTY_SONIC") | not' "$_nft_tmp/minimal.json" >/dev/null 2>&1; then + pass "$test_name" + else + fail "$test_name" + fi +} + +{ + test_name="native-file-tools: an explicit ENABLE_NATIVE_FILE_TOOLS=false drops the env key from a standard build" + if _nft_build_settings standard "$_nft_tmp/standard-off.json" ENABLE_NATIVE_FILE_TOOLS=false \ + && jq -e '(.env // {}) | has("CLAUDE_CODE_THRIFTY_SONIC") | not' "$_nft_tmp/standard-off.json" >/dev/null 2>&1 \ + && jq -e '.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS == "1"' "$_nft_tmp/standard-off.json" >/dev/null 2>&1; then + pass "$test_name" + else + fail "$test_name" + fi +} + +# ── Update path: older installs get the profile default, explicit choices win ── + +# Usage: _nft_restore → prints ENABLE_NATIVE_FILE_TOOLS +# shellcheck disable=SC2034 # globals below are consumed by the sourced wizard +_nft_restore() { + local manifest_json="$1" conf_body="$2" + ( + set -euo pipefail + _home="$(mktemp -d)" + trap 'rm -rf "$_home"' EXIT + export HOME="$_home" + mkdir -p "$HOME/.claude" + printf '%s\n' "$manifest_json" > "$HOME/.claude/.starter-kit-manifest.json" + STR_ENABLED="Enabled"; STR_DISABLED="Disabled" + source "$PROJECT_DIR/wizard/wizard.sh" + # wizard.sh resets WIZARD_CONFIG_FILE while loading, so point it at the + # saved conf only afterwards (same order as setup.sh: source, then parse). + WIZARD_CONFIG_FILE="$HOME/test.conf" + if [[ -n "$conf_body" ]]; then printf '%s\n' "$conf_body" > "$WIZARD_CONFIG_FILE"; fi + PROFILE=""; LANGUAGE=""; EDITOR_CHOICE=""; SELECTED_PLUGINS="" + COMMIT_ATTRIBUTION=""; ENABLE_NEW_INIT=""; ENABLE_NATIVE_FILE_TOOLS="" + _CLI_OVERRIDES=() + _restore_config_from_manifest >/dev/null 2>&1 + printf '%s' "${ENABLE_NATIVE_FILE_TOOLS:-}" + ) +} + +{ + test_name="native-file-tools: update of an older standard install enables the feature (profile default)" + if [[ "$(_nft_restore '{"profile":"standard","language":"en","editor":"none","plugins":""}' 'ENABLE_SAFETY_NET="true"')" == "true" ]]; then + pass "$test_name" + else + fail "$test_name" + fi +} + +{ + test_name="native-file-tools: update of an older minimal install keeps the feature off" + if [[ "$(_nft_restore '{"profile":"minimal","language":"en","editor":"none","plugins":""}' 'INSTALL_RULES="true"')" == "false" ]]; then + pass "$test_name" + else + fail "$test_name" + fi +} + +{ + test_name="native-file-tools: update of an older custom install fills the intended default (true)" + if [[ "$(_nft_restore '{"profile":"custom","language":"en","editor":"none","plugins":""}' 'ENABLE_TMUX_HOOKS="true"')" == "true" ]]; then + pass "$test_name" + else + fail "$test_name" + fi +} + +{ + test_name="native-file-tools: a saved explicit ENABLE_NATIVE_FILE_TOOLS=false survives update on every profile" + _nft_ok=true + for _nft_profile in standard full custom; do + [[ "$(_nft_restore "{\"profile\":\"$_nft_profile\",\"language\":\"en\",\"editor\":\"none\",\"plugins\":\"\"}" 'ENABLE_NATIVE_FILE_TOOLS="false"')" == "false" ]] || _nft_ok=false + done + if [[ "$_nft_ok" == "true" ]]; then + pass "$test_name" + else + fail "$test_name" + fi +} + +# Interactive wizard, "reuse saved config" branch: fill_missing_profile_defaults +# is the only default fill on that path, and custom has no profile conf. +# Usage: _nft_reuse_fill → prints ENABLE_NATIVE_FILE_TOOLS +# shellcheck disable=SC2034 # globals consumed by the sourced wizard +_nft_reuse_fill() { + local profile="$1" preset="$2" + ( + set -euo pipefail + STR_ENABLED="Enabled"; STR_DISABLED="Disabled" + source "$PROJECT_DIR/wizard/wizard.sh" + ENABLE_NATIVE_FILE_TOOLS="$preset" + ENABLE_AGENT_TEAMS="" + PROFILE="$profile" + fill_missing_profile_defaults "$profile" >/dev/null 2>&1 + printf '%s|%s' "${ENABLE_NATIVE_FILE_TOOLS:-}" "${ENABLE_AGENT_TEAMS:-}" + ) +} + +{ + test_name="native-file-tools: saved-config reuse on a custom profile fills native-file-tools and agent-teams (true)" + if [[ "$(_nft_reuse_fill custom "")" == "true|true" ]]; then + pass "$test_name" + else + fail "$test_name" + fi +} + +{ + test_name="native-file-tools: saved-config reuse keeps an explicit false and honours the minimal profile default" + if [[ "$(_nft_reuse_fill custom false)" == "false|true" ]] \ + && [[ "$(_nft_reuse_fill minimal "")" == "false|true" ]] \ + && [[ "$(_nft_reuse_fill standard "")" == "true|true" ]]; then + pass "$test_name" + else + fail "$test_name" + fi +} + +# Non-interactive fresh install with --profile=custom (no profile conf): the +# default fill must give the key its intended value, and an explicit CLI/conf +# value must survive. +# Usage: _nft_noninteractive_fill → prints ENABLE_NATIVE_FILE_TOOLS +# shellcheck disable=SC2034 # globals consumed by the sourced wizard +_nft_noninteractive_fill() { + local preset="$1" + ( + set -euo pipefail + STR_ENABLED="Enabled"; STR_DISABLED="Disabled" + source "$PROJECT_DIR/wizard/wizard.sh" + PROFILE="custom"; LANGUAGE="en"; EDITOR_CHOICE="none" + SELECTED_PLUGINS=""; _SELECTED_PLUGINS_EXPLICIT="true" + ENABLE_NATIVE_FILE_TOOLS="$preset" + _CLI_OVERRIDES=() + _fill_noninteractive_defaults >/dev/null 2>&1 + printf '%s' "${ENABLE_NATIVE_FILE_TOOLS:-}" + ) +} + +{ + test_name="native-file-tools: non-interactive custom fresh install defaults the key to true and keeps an explicit false" + if [[ "$(_nft_noninteractive_fill "")" == "true" ]] \ + && [[ "$(_nft_noninteractive_fill false)" == "false" ]]; then + pass "$test_name" + else + fail "$test_name" + fi +} + +{ + test_name="native-file-tools: --hooks CSV token native-tools toggles the key" + if ( + set -euo pipefail + # shellcheck disable=SC2034 # consumed by wizard.sh label helpers + STR_ENABLED="Enabled" + # shellcheck disable=SC2034 + STR_DISABLED="Disabled" + source "$PROJECT_DIR/wizard/wizard.sh" + _apply_hooks_csv "native-tools" + [[ "${ENABLE_NATIVE_FILE_TOOLS:-}" == "true" ]] + _apply_hooks_csv "safety-net" + [[ "${ENABLE_NATIVE_FILE_TOOLS:-}" == "false" ]] + ); then + pass "$test_name" + else + fail "$test_name" + fi +} + +rm -rf "$_nft_tmp" +unset _nft_ok _nft_profile _nft_tmp diff --git a/wizard/defaults.conf b/wizard/defaults.conf index 450e07d..22530be 100644 --- a/wizard/defaults.conf +++ b/wizard/defaults.conf @@ -19,6 +19,7 @@ ENABLE_BIOME_HOOKS="false" ENABLE_PR_CREATION_LOG="false" ENABLE_GHOSTTY_SETUP="false" ENABLE_AGENT_TEAMS="true" +ENABLE_NATIVE_FILE_TOOLS="true" ENABLE_FONTS_SETUP="false" SELECTED_PLUGINS="" diff --git a/wizard/registry.sh b/wizard/registry.sh index 2cb855d..864fe58 100644 --- a/wizard/registry.sh +++ b/wizard/registry.sh @@ -21,6 +21,7 @@ _CONFIG_KEYS=( ENABLE_PRE_COMPACT_COMMIT ENABLE_SAFETY_NET ENABLE_AUTO_UPDATE ENABLE_WEB_CONTENT_UPDATE ENABLE_STATUSLINE ENABLE_GHOSTTY_SETUP ENABLE_FONTS_SETUP ENABLE_DOC_SIZE_GUARD ENABLE_NO_FLICKER ENABLE_FEATURE_RECOMMENDATION ENABLE_AGENT_TEAMS + ENABLE_NATIVE_FILE_TOOLS "" DISMISSED_FEATURES "" @@ -231,6 +232,7 @@ HOOK_KEYS=( "ENABLE_DOC_SIZE_GUARD" "ENABLE_FEATURE_RECOMMENDATION" "ENABLE_AGENT_TEAMS" + "ENABLE_NATIVE_FILE_TOOLS" ) HOOK_TOKENS=( @@ -246,6 +248,7 @@ HOOK_TOKENS=( "doc-size" "feature-rec" "agent-teams" + "native-tools" ) HOOK_LABELS=() @@ -264,9 +267,20 @@ _init_hook_labels() { "${STR_HOOKS_DOC_SIZE:-Doc Size Guard - Warn when CLAUDE.md/AGENTS.md is too large}" "${STR_HOOKS_FEATURE_RECOMMENDATION:-Feature Recommendation - Notify about new features on session start}" "${STR_HOOKS_AGENT_TEAMS:-Agent Teams (experimental) - Enable Claude Code agent teams}" + "${STR_HOOKS_NATIVE_FILE_TOOLS:-Native File Tools - Keep Read/Edit/Write as the primary file tools in auto / bypassPermissions sessions (CLAUDE_CODE_THRIFTY_SONIC=0)}" ) } +# Feature keys introduced after older installs. minimal/standard/full receive +# the value from their profile conf; a custom profile has no conf, so without +# this fill an update would leave the key empty and the 3-way merge would drop +# the env fragment (#138). A saved explicit value is never overwritten. +_fill_late_feature_defaults() { + [[ -z "${ENABLE_AGENT_TEAMS:-}" ]] && ENABLE_AGENT_TEAMS="true" + [[ -z "${ENABLE_NATIVE_FILE_TOOLS:-}" ]] && ENABLE_NATIVE_FILE_TOOLS="true" + return 0 +} + _apply_hooks_csv() { local csv="$1" local i diff --git a/wizard/steps.sh b/wizard/steps.sh index 66f5bc4..b9f74f5 100644 --- a/wizard/steps.sh +++ b/wizard/steps.sh @@ -298,6 +298,7 @@ _step_confirm() { printf "%-20s : %s\n" "$STR_CONFIRM_STATUSLINE" "$(_bool_label_enabled "${ENABLE_STATUSLINE:-false}")" printf "%-20s : %s\n" "$STR_CONFIRM_NO_FLICKER" "$(_bool_label_enabled "${ENABLE_NO_FLICKER:-false}")" printf "%-20s : %s\n" "$STR_CONFIRM_AGENT_TEAMS" "$(_bool_label_enabled "${ENABLE_AGENT_TEAMS:-false}")" + printf "%-20s : %s\n" "${STR_CONFIRM_NATIVE_FILE_TOOLS:-Native File Tools}" "$(_bool_label_enabled "${ENABLE_NATIVE_FILE_TOOLS:-false}")" printf "%-20s : %s\n" "${STR_CONFIRM_FEATURE_RECOMMENDATION:-Feature Rec.}" "$(_bool_label_enabled "${ENABLE_FEATURE_RECOMMENDATION:-false}")" local hook_labels=() @@ -363,7 +364,7 @@ _fill_noninteractive_defaults() { [[ -z "$ENABLE_NEW_INIT" ]] && ENABLE_NEW_INIT="true" [[ -z "${ENABLE_STATUSLINE:-}" ]] && ENABLE_STATUSLINE="true" [[ -z "${ENABLE_NO_FLICKER:-}" ]] && ENABLE_NO_FLICKER="false" - [[ -z "${ENABLE_AGENT_TEAMS:-}" ]] && ENABLE_AGENT_TEAMS="true" + _fill_late_feature_defaults [[ -z "${ENABLE_FEATURE_RECOMMENDATION:-}" ]] && ENABLE_FEATURE_RECOMMENDATION="false" [[ -z "${ENABLE_WEB_CONTENT_UPDATE:-}" ]] && ENABLE_WEB_CONTENT_UPDATE="false" [[ -z "$ENABLE_GHOSTTY_SETUP" ]] && ENABLE_GHOSTTY_SETUP="false" diff --git a/wizard/wizard.sh b/wizard/wizard.sh index 1460398..8fbe412 100755 --- a/wizard/wizard.sh +++ b/wizard/wizard.sh @@ -49,6 +49,7 @@ ENABLE_FONTS_SETUP="${ENABLE_FONTS_SETUP:-}" ENABLE_DOC_SIZE_GUARD="${ENABLE_DOC_SIZE_GUARD:-}" ENABLE_NO_FLICKER="${ENABLE_NO_FLICKER:-}" ENABLE_AGENT_TEAMS="${ENABLE_AGENT_TEAMS:-}" +ENABLE_NATIVE_FILE_TOOLS="${ENABLE_NATIVE_FILE_TOOLS:-}" ENABLE_FEATURE_RECOMMENDATION="${ENABLE_FEATURE_RECOMMENDATION:-}" DISMISSED_FEATURES="${DISMISSED_FEATURES:-}" @@ -334,6 +335,7 @@ fill_missing_profile_defaults() { _load_profile_preserving_values "$profile" _rc=$? [[ "$_rc" -eq 0 ]] || return "$_rc" + _fill_late_feature_defaults # custom has no conf; late keys must not stay empty _normalize_formatter_hooks "$_PROFILE_FILL_FORMATTER_PREFER" } @@ -455,11 +457,9 @@ _restore_config_from_manifest() { ENABLE_CODEX_PLUGIN="$manifest_codex_plugin" fi - # Keys introduced after older installs get their intended default here. - # Profile conf covers minimal/standard/full, but custom has no conf file, - # so without this fill a custom-profile update would silently drop the - # agent-teams env fragment (3-way merge would then delete the key). - [[ -z "${ENABLE_AGENT_TEAMS:-}" ]] && ENABLE_AGENT_TEAMS="true" + # Keys introduced after older installs get their intended default here + # (custom has no profile conf); see _fill_late_feature_defaults in registry.sh. + _fill_late_feature_defaults _normalize_formatter_hooks _normalize_codex_state From d20808b047af1ae9d21622c319e3a4d37ef876a1 Mon Sep 17 00:00:00 2001 From: okash1n <48118431+okash1n@users.noreply.github.com> Date: Mon, 7 Sep 2026 09:48:56 +0900 Subject: [PATCH 2/2] =?UTF-8?q?test:=20native-file-tools=20=E3=83=86?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=81=AE=E3=82=A2=E3=82=B5=E3=83=BC=E3=82=B7?= =?UTF-8?q?=E3=83=A7=E3=83=B3=E3=82=92=E6=8B=98=E6=9D=9F=E5=8C=96=E3=81=97?= =?UTF-8?q?=E3=80=81=E3=83=8F=E3=83=BC=E3=83=8D=E3=82=B9=E3=81=A7=E5=AE=9F?= =?UTF-8?q?=E7=92=B0=E5=A2=83=E3=81=AE=20session=20hook=20=E3=82=92?= =?UTF-8?q?=E6=8B=92=E5=90=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit レビューで確認した 3 件に対応した。 - tests/unit/test-native-file-tools.sh: `if ( set -e; ... )` の条件内では set -e が効かず、最後のコマンドの終了コードしか判定に使われない。 レジストリ・hook トークン・--hooks CSV の各アサーションに `|| exit 1` を 付けて拘束化した。main(修正前)に対する失敗件数は 14 件から 15 件になる - tests/manual/bash-first-steer/run-case.sh: BFS_KIT_SETTINGS に渡した kit 生成 settings.json が SessionStart / SessionEnd フックを含む場合は 拒否する。フックコマンドは実環境の ~/.claude/hooks/ を指すため、 Standard / Full の既定値では auto-update(git pull + setup.sh --update) などが fixture セッションから実インストールに対して走ってしまう - ハーネスの README と CHANGELOG の記述を実測に合わせて修正 (修正前の失敗件数 12 件 → 17 件中 15 件、リリース日 2026-09-07) Claude-Session: https://claude.ai/code/session_01C9mrbbXQgV9fJ8Zy5UoEYc --- CHANGELOG.md | 6 +++--- tests/manual/bash-first-steer/README.md | 20 +++++++++++++------ tests/manual/bash-first-steer/run-case.sh | 11 +++++++++++ tests/unit/test-native-file-tools.sh | 24 ++++++++++++----------- 4 files changed, 41 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 723ab88..579032b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,15 +4,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/). -## [0.78.0] - 2026-09-06 +## [0.78.0] - 2026-09-07 ### Added - **Bash 優先指示の解除フック `native-file-tools` を追加(Standard / Full 既定有効)**: Claude Code 2.1.261 は auto / bypassPermissions モードのセッションに「ファイルの読み書きは Read/Edit/Write ではなく cat / sed / heredoc で行え」という指示を注入する(バイナリ内の未文書化 feature flag `CLAUDE_CODE_THRIFTY_SONIC`。Fable 5.1 系モデルでは強制有効、Opus 5 は GrowthBook のコホート配信、それ以外のモデルは既定で無効だが、サーバー配信の clientData やモデル別 capability からも強制され得る条件式になっている)。この状態ではモデルが Read/Edit/Write をほぼ使わないため、キットの `PostToolUse` `Edit|Write`(prettier-hooks / biome-hooks)、`PreToolUse` `Write`(doc-blocker)、`PostToolUse` `Write`(doc-size-guard)が発火せず、`paths:` 付き Rules とサブディレクトリの CLAUDE.md も読み込まれず、native checkpoint / rewind の追跡対象にもならない。新フックは `settings.json` の `env` に `CLAUDE_CODE_THRIFTY_SONIC="0"` を入れて指示の注入を解除する - **実 CLI で再現・修正確認済み**: Claude Code 2.1.261 / claude-fable-5-1 / `--permission-mode auto` / `--setting-sources project`(利用者のグローバル設定・プラグイン・MCP を読み込まない隔離)で、同一の合成リポジトリと課題を新規セッションで比較した。フラグ未指定では 3 セッション中 3 セッションがツール呼び出しを Bash のみで済ませ、Biome 整形なし・doc-blocker の確認なし・doc-size-guard の警告なし・`InstructionsLoaded` イベントは session_start の 2 件のみ(paths Rules / nested CLAUDE.md の遅延ロードなし)。`env.CLAUDE_CODE_THRIFTY_SONIC="0"` では 3 セッション中 3 セッションが Read / Write を使い、Biome 整形・doc-blocker の `ask`・doc-size-guard の警告・`path_glob_match` / `nested_traversal` のロードがすべて観測された。トランスクリプトに永続化される `auto_mode` attachment の `bashFirst` 値(true / 消失)でも指示の有無を直接確認した。キットが生成した `settings.json` をそのまま使った検証でも同じ結果を得た - **保証範囲と残る制約**: 復元されるのは通常のツール選択であって、Bash の使用が禁止されるわけではない。利用者が「Bash で編集して」と指示した対照ケースでは、フラグを入れても Bash のみで編集され、フック・paths Rules は従来どおり発火しない。auto / bypassPermissions 以外のモードでは元々この指示は注入されない(バイナリの条件式が他モードで即座に空を返す。`acceptEdits` は `"1"` を入れても注入されないことを実測で確認、`default` / `plan` は条件式からの判断で実測はしていない)。doc-blocker / doc-size-guard が Edit に反応しない点は従来からの制限で、今回の対象外 - **配布形態**: `agent-teams` / `no-flicker` と同じ env-only feature(`features/native-file-tools/hooks.json`)。`ENABLE_NATIVE_FILE_TOOLS` をレジストリ・profiles・wizard(フック選択の `native-tools` トークン、確認画面、非対話デフォルト)・MDM の許可キーに登録した。Standard / Full は既定 `true`、Minimal は `false`。既存インストールは `setup.sh --update` / 自動アップデートでプロファイル既定を受け取り、custom プロファイルは agent-teams と同じく `true` で補完する。`~/.claude-starter-kit.conf` に明示的に書かれた値は上書きしない。Claude Code 側でフラグが撤去された場合はフラグメントからキーを外すだけでよく、次回アップデートの 3-way merge がこのキーを変更していない利用者の `settings.json` から除去する(#120 の effortLevel 撤去と同じ kit-removed 経路。自分で値を変更していた利用者の値は従来どおり保持され、残っても未知の env は Claude Code に無視される) - - **回帰テスト**: `tests/unit/test-native-file-tools.sh`(Standard / Full の生成 `settings.json` に env キーが入ること、Minimal と明示 `false` では入らないこと、更新経路でのプロファイル既定補完と明示値の保持、`--hooks` トークン)。修正前のコードでは 12 件が失敗する - - **実 CLI 検証ハーネスを同梱(CI 非連動)**: `tests/manual/bash-first-steer/` に合成 fixture 生成・ケース実行・集計スクリプトと期待結果表を置いた。モデルを実際に呼ぶため通常 CI には組み込まない + - **回帰テスト**: `tests/unit/test-native-file-tools.sh`(Standard / Full の生成 `settings.json` に env キーが入ること、Minimal と明示 `false` では入らないこと、更新経路でのプロファイル既定補完と明示値の保持、`--hooks` トークン)。修正前のコードでは 17 件中 15 件が失敗する + - **実 CLI 検証ハーネスを同梱(CI 非連動)**: `tests/manual/bash-first-steer/` に合成 fixture 生成・ケース実行・集計スクリプトと期待結果表を置いた。モデルを実際に呼ぶため通常 CI には組み込まない。キット生成の `settings.json` をそのまま検証するモードでは、実環境の `~/.claude/hooks/` を動かしてしまう `SessionStart` / `SessionEnd` フック(auto-update・web-content-update・機能レコメンド)を含むファイルを拒否する ### Changed - **README のセキュリティ機能・native rewind の説明を実測に合わせて修正**: security-guidance の「編集時」パターン警告は Edit / Write ツール経由の変更にだけ働き、Bash 経由の変更はターン終了時の git diff レビューとコミット時レビューで拾うことを明記した(導入版 2.0.7 の `hooks.json` / `security_reminder_hook.py` で確認)。コンパクト前スナップショットの説明と `commands/checkpoint.md` に、native rewind はファイル編集ツール(Edit / Write / NotebookEdit)による変更しか復元しない(公式ドキュメント「Checkpointing」の Limitations)ことを追記した diff --git a/tests/manual/bash-first-steer/README.md b/tests/manual/bash-first-steer/README.md index 27b3bdd..bce1bb5 100644 --- a/tests/manual/bash-first-steer/README.md +++ b/tests/manual/bash-first-steer/README.md @@ -41,9 +41,12 @@ Each case runs `claude -p` with `--setting-sources project --strict-mcp-config` in a fresh copy of a synthetic git repository, so `~/.claude/settings.json`, `~/.claude/CLAUDE.md`, `~/.claude/rules/`, plugins and MCP servers are not loaded (verified via the `InstructionsLoaded` observer: only the fixture's own -files appear). The user's real configuration is never modified. Session -transcripts are still written under `~/.claude/projects/` (synthetic content -only); each run directory records the path. +files appear). In the default mode every hook the session runs lives inside the +experiment directory, so the user's real configuration is never modified. In +`BFS_KIT_SETTINGS` mode the hook commands are the real `~/.claude/hooks/*` +scripts (see below). Session transcripts are still written under +`~/.claude/projects/` (synthetic content only); each run directory records the +path. Observables per run (`runs//`): @@ -85,9 +88,14 @@ To verify a kit-generated `settings.json` verbatim (for example the output of `setup.sh --profile=full --hooks=doc-block,biome,doc-size,native-tools` in a throwaway `HOME`), pass it with `BFS_KIT_SETTINGS=` and `flag=unset`; observers are then layered on with `--settings` so the file under test is -byte-identical to the kit output. Drop the `SessionStart` reader first if the -throwaway `HOME` is not the runtime `HOME` (it would read the real pending -file). +byte-identical to the kit output. The hook commands in that file are absolute +paths under the real `~/.claude/hooks/`, so they must already resolve on this +machine, and `run-case.sh` refuses a file that contains `SessionStart` / +`SessionEnd` hooks: a stock Standard/Full `settings.json` would otherwise run +the real auto-update hook (`git pull` + `setup.sh --update` on the real +install), the web-content-update hook and the feature-recommendation reader +inside the fixture session. Generate the file with a `--hooks` list that +leaves those out, as in the example above. ## Expected outcomes (Claude Code 2.1.261, claude-fable-5-1, 2026-09-06) diff --git a/tests/manual/bash-first-steer/run-case.sh b/tests/manual/bash-first-steer/run-case.sh index bde7b95..e755600 100644 --- a/tests/manual/bash-first-steer/run-case.sh +++ b/tests/manual/bash-first-steer/run-case.sh @@ -52,6 +52,17 @@ if [[ -n "${BFS_KIT_SETTINGS:-}" ]]; then echo "mode=bypass is not usable with a kit settings.json (disableBypassPermissionsMode=disable)" >&2 exit 1 fi + # The kit's hook commands are absolute paths under the real ~/.claude/hooks, + # so a Standard/Full settings.json would run the real SessionStart/SessionEnd + # hooks inside the fixture session: auto-update (git pull + setup.sh --update + # on the real install), web-content-update (npm update of the real skill + # deps) and the feature-recommendation reader. None of them is under test, + # so refuse instead of mutating the user's install. + if jq -e '((.hooks.SessionStart // []) | length) + ((.hooks.SessionEnd // []) | length) > 0' \ + "$BFS_KIT_SETTINGS" >/dev/null 2>&1; then + echo "kit settings.json contains SessionStart/SessionEnd hooks (auto-update, web-content-update, feature-recommendation); they would run against the real ~/.claude. Generate it with a --hooks list that leaves them out, e.g. --hooks=doc-block,biome,doc-size,native-tools" >&2 + exit 1 + fi fi [[ -d "$T" ]] || { echo "fixture template missing; run make-fixture.sh first" >&2; exit 1; } [[ -e "$OUT" ]] && { echo "run dir exists: $OUT" >&2; exit 1; } diff --git a/tests/unit/test-native-file-tools.sh b/tests/unit/test-native-file-tools.sh index 8b1fb40..7f0d436 100644 --- a/tests/unit/test-native-file-tools.sh +++ b/tests/unit/test-native-file-tools.sh @@ -70,13 +70,15 @@ _nft_build_settings() { { test_name="native-file-tools: feature registry maps native-file-tools to ENABLE_NATIVE_FILE_TOOLS without scripts" + # `set -e` is inert inside an `if ( ... )` condition, so every assertion + # must exit explicitly; otherwise only the last command's status counts. if ( set -euo pipefail source "$PROJECT_DIR/lib/features.sh" - [[ "${_FEATURE_FLAGS[native-file-tools]:-}" == "ENABLE_NATIVE_FILE_TOOLS" ]] - [[ " ${_FEATURE_ORDER[*]} " == *" native-file-tools "* ]] - [[ -z "${_FEATURE_HAS_SCRIPTS[native-file-tools]:-}" ]] - [[ ! -d "$_nft_feature_dir/scripts" ]] + [[ "${_FEATURE_FLAGS[native-file-tools]:-}" == "ENABLE_NATIVE_FILE_TOOLS" ]] || exit 1 + [[ " ${_FEATURE_ORDER[*]} " == *" native-file-tools "* ]] || exit 1 + [[ -z "${_FEATURE_HAS_SCRIPTS[native-file-tools]:-}" ]] || exit 1 + [[ ! -d "$_nft_feature_dir/scripts" ]] || exit 1 ); then pass "$test_name" else @@ -102,16 +104,16 @@ _nft_build_settings() { set -euo pipefail source "$PROJECT_DIR/i18n/en/strings.sh" source "$PROJECT_DIR/wizard/registry.sh" - [[ " ${_CONFIG_KEYS[*]} " == *" ENABLE_NATIVE_FILE_TOOLS "* ]] + [[ " ${_CONFIG_KEYS[*]} " == *" ENABLE_NATIVE_FILE_TOOLS "* ]] || exit 1 _idx="" for _i in "${!HOOK_KEYS[@]}"; do [[ "${HOOK_KEYS[$_i]}" == "ENABLE_NATIVE_FILE_TOOLS" ]] && _idx="$_i" done - [[ -n "$_idx" ]] - [[ "${HOOK_TOKENS[$_idx]}" == "native-tools" ]] + [[ -n "$_idx" ]] || exit 1 + [[ "${HOOK_TOKENS[$_idx]:-}" == "native-tools" ]] || exit 1 _init_hook_labels - [[ -n "${HOOK_LABELS[$_idx]}" ]] - [[ "${HOOK_LABELS[$_idx]}" == *"THRIFTY_SONIC"* ]] + [[ -n "${HOOK_LABELS[$_idx]:-}" ]] || exit 1 + [[ "${HOOK_LABELS[$_idx]}" == *"THRIFTY_SONIC"* ]] || exit 1 ); then pass "$test_name" else @@ -321,9 +323,9 @@ _nft_noninteractive_fill() { STR_DISABLED="Disabled" source "$PROJECT_DIR/wizard/wizard.sh" _apply_hooks_csv "native-tools" - [[ "${ENABLE_NATIVE_FILE_TOOLS:-}" == "true" ]] + [[ "${ENABLE_NATIVE_FILE_TOOLS:-}" == "true" ]] || exit 1 _apply_hooks_csv "safety-net" - [[ "${ENABLE_NATIVE_FILE_TOOLS:-}" == "false" ]] + [[ "${ENABLE_NATIVE_FILE_TOOLS:-}" == "false" ]] || exit 1 ); then pass "$test_name" else