diff --git a/CHANGELOG.md b/CHANGELOG.md index 8baa7cd..6224d5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,47 +8,47 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Added -- **Tests** (`tests/`): 65 tests covering the six hook scripts. Each runs the real script in a subprocess against a crafted payload and asserts on the decision it emits — nothing is mocked, since "the hook silently stopped firing" is the regression worth catching. Both hook bugs this project has shipped were **logic** errors, not shell errors, and neither `shellcheck` nor `validate_config.py` can see that class of fault. Validated by mutation: reintroducing the pre-v1.3.0 `\b` boundary in `protect-main.sh` turns 8 tests red in both directions — `rm -rf /`, `rm -rf .`, `rm -rf ~` slipping through, and `rm -rf .git`, `rm -rf ~/tmp-dir` wrongly blocked. Two paths are deliberately uncovered and documented as such: `verify.sh`'s ruff/pytest body (running it from inside pytest would recurse) and `auto-lint.sh`'s formatting body (its outcome depends on the surrounding project's ruff `include`) -- **CI** (`.github/workflows/ci.yml`): a GitHub Actions workflow on pull requests and pushes to `main`. Until now nothing verified this repository at all — every PR merged without a single automated check. It runs ruff (lint + format), `shellcheck --severity=warning` over the hook scripts, and a new configuration validator -- **Scripts** (`scripts/validate_config.py`): validates that the configuration this template ships is internally coherent — `settings.json` and the MCP configs parse; hook scripts referenced by `settings.json` exist on disk; `CLAUDE.md`'s `@`-imports resolve; every skill's `SKILL.md` still declares `name` and `description`. All but the first fail **silently** at runtime: a hook whose script was renamed just stops firing, a broken `@`-import drops that rule from Claude's context, and a skill missing frontmatter becomes undiscoverable — none of which surfaces an error. Verified by injecting each fault and confirming a non-zero exit +- **Tests** (`tests/`): 65 tests covering the six hook scripts. Each runs the real script in a subprocess against a crafted payload and asserts on the decision it emits — nothing is mocked, since "the hook silently stopped firing" is the regression worth catching and a mock cannot fail that way. Validated by mutation: reintroducing the pre-v1.3.0 `\b` boundary in `protect-main.sh` turns 8 tests red in both directions. `verify.sh`'s ruff/pytest body and `auto-lint.sh`'s formatting body are left uncovered on purpose +- **CI** (`.github/workflows/ci.yml`): GitHub Actions workflow on pull requests and pushes to `main`, running ruff (lint + format), `scripts/validate_config.py`, `shellcheck --severity=warning` over the hook scripts, and `pytest`. Nothing verified this repository before — every earlier pull request merged without a single automated check +- **Scripts** (`scripts/validate_config.py`): checks that `settings.json` and the MCP configs parse, that the hook scripts `settings.json` references exist, that `CLAUDE.md`'s `@`-imports resolve, and that every skill's `SKILL.md` declares `name` and `description`. All but the first fail silently at runtime. Verified by injecting each fault and confirming a non-zero exit +- **README.md**: a "Bash Sandbox" section, with a paste-ready `sandbox` block for a uv-based project. Deliberately not enabled in `.claude/settings.json`: the sandbox does not run on native Windows, and enabling it in checked-in project settings would warn at startup for every Windows contributor +- **README.md**: `bubblewrap` and `socat` added to the optional dependencies table, needed by the sandbox on Linux and WSL2 +- **.env.example** / **README.md**: six Claude Code tuning variables — `BASH_DEFAULT_TIMEOUT_MS`, `BASH_MAX_OUTPUT_LENGTH`, `CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH`, `CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS`, `CLAUDE_CODE_MAX_WEB_SEARCHES_PER_SESSION` and `CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS`. The first two are the ones a pytest-based project hits in practice — a suite over two minutes gets killed mid-run, and verbose output truncates before the failure summary; the rest are recorded at their defaults -- **README.md**: a "Bash Sandbox" section documenting Claude Code's OS-enforced filesystem and network isolation for Bash commands, with a paste-ready `sandbox` block for a uv-based Python project (allowlists PyPI and GitHub so `uv sync`/`uv add`/`git` don't prompt; denies reads of `~/.aws/credentials` and `~/.ssh`). Deliberately **not** enabled in `.claude/settings.json`: the sandbox doesn't run on native Windows, and enabling it in checked-in project settings would produce a startup warning for every Windows contributor — so the section recommends user-level settings instead. Documents the two footguns worth knowing up front: there's no built-in credential deny list (only what you list is protected), and the `dangerouslyDisableSandbox` retry can put a failed command back outside the boundary unless `allowUnsandboxedCommands` is `false` -- **README.md**: added `bubblewrap` + `socat` to the optional dependencies table — needed for the sandbox on Linux/WSL2, while macOS uses the built-in Seatbelt framework -- **.env.example** / **README.md**: six Claude Code tuning variables. `BASH_DEFAULT_TIMEOUT_MS` (default `120000`) and `BASH_MAX_OUTPUT_LENGTH` (default `30000`, max `150000`) matter for this template specifically — a test suite running over two minutes gets killed mid-run, and verbose `pytest -v` output can be truncated before the failure summary. The other four are listed as explicit defaults: `CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH` (`3`), `CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS` (`20`, v2.1.217+), `CLAUDE_CODE_MAX_WEB_SEARCHES_PER_SESSION` (`200`, v2.1.212+ — shared across the main conversation and every subagent, so parallel research fan-outs draw on one budget; raisable but not disableable, and `/clear` resets it), and `CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS` (`120000`, v2.1.212+ — how long a main-conversation MCP call runs before moving to a background task; `0` disables, and subagent calls are never backgrounded) +### Changed + +- **Skills** (`.claude/skills/`): re-synced the ten skills vendored from [anthropics/skills](https://github.com/anthropics/skills) — `claude-api`, `doc-coauthoring`, `docx`, `frontend-design`, `mcp-builder`, `pdf`, `pptx`, `skill-creator`, `webapp-testing`, `xlsx` — against upstream `f17010c`. They had been copied on 2026-03-20 and never refreshed, accumulating five months of upstream change: + - **`claude-api`** was the consequential one: it still told Claude to default to `claude-opus-4-6`, and its `shared/models.md` predated Opus 5, Sonnet 5 and Fable 5 — a reference skill steering code toward superseded model IDs. Upstream also split the per-language docs into directories, replaced the `*/agent-sdk/` pages with `shared/agent-design.md`, and added the Managed Agents doc set + - **`docx`, `pptx`, `xlsx`** picked up upstream's 2026-07-17 consolidation of the shared `scripts/office/` helpers, which carries security fixes, plus `.dotx`/`.potx`/`.xltx` template support + - **`frontend-design`** was rewritten upstream from "production-grade interfaces" into visual-design direction + - The remaining skills changed only in `LICENSE.txt`, or not at all +- **pyproject.toml**: ruff's `include` was `["pyproject.toml", "src/**/*.py"]`, which matched nothing this repository actually ships, since there is no `src/`. Extended to `scripts/**/*.py`, `tests/**/*.py` and `.claude/statusline.py`, with `tests` added to `known-first-party`. Vendored `.claude/skills/` scripts stay out of scope +- **README.md**: recorded which ten skills are vendored copies from upstream, that they do not self-update, and the sync point (`f17010c`, 2026-08-13) so the next refresh has a baseline. Refreshed the `claude-api`, `docx`, `pptx`, `skill-creator` and `frontend-design` rows to match their new upstream descriptions +- **README.md**: rewrote the passages that narrated change instead of describing the current state — the fullscreen-rendering and background-subagent defaults, the permission-mode label, and the rationale for the hook tests. Change history belongs in this file, not in the README +- **CONTRIBUTING.md**: the contributor verification command ran ruff alone while CI runs four checks, so a contributor could be green locally and red in CI. Aligned both commands, noted that `shellcheck` runs in CI, and recorded that the hook tests need `bash` and `jq` on PATH (they skip without them) +- **README.md**: replaced the "many more events" aside in the hooks section with the current count (32) and the events most useful for extending this setup +- **README.md**: documented that `permissions.defaultMode: "default"` is labeled "Manual" in the interface, with `"manual"` accepted as an alias +- **Settings** (`.claude/settings.json`): added `permissions.defaultMode: "default"` explicitly — same behavior as the implicit default, but now visible and easy to customize +- **Settings** (`.claude/settings.json`): `fallbackModel` refreshed from the stale `claude-sonnet-4-6`/`claude-haiku-4-5` IDs to the current `claude-sonnet-5`/`claude-haiku-4-5-20251001` +- **Hooks** (`enforce-uv.sh`, `protect-main.sh`): added `if` conditions to their `PreToolUse` entries so they only spawn on matching Bash commands (Python/pip tooling, git/rm) instead of every Bash call +- **.env.example**: added `CLAUDE_CODE_DISABLE_BUNDLED_SKILLS="0"` (explicit default; set to `1` to hide Claude Code's own bundled skills and workflows, project `.claude/skills/` is unaffected) +- **.env.example**: `CLAUDE_CODE_NO_FLICKER` default flipped to `1` to match the current Claude Code default +- **README.md**: documented `permissions.disableAutoMode` and `language` as available but intentionally unset — neither has a neutral value that preserves default behavior +- **README.md**: synced the `fallbackModel` example, documented the hook `if` conditional field, updated the `CLAUDE_CODE_NO_FLICKER` and `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS` rows, and bumped the RTK reference to v0.43.0 with `rtk gain` ### Removed -- **Commands** (`.claude/commands/code-review.md`): deleted. `/code-review` became a built-in Claude Code command (v2.1.218 runs it as a background subagent, v2.1.223 made `/review` its alias and added reusable effort levels such as `/code-review high`), and a project command of the same name shadows it — the built-in was missing from the session's skill list until the file was removed. The built-in supersedes this copy: it does the same multi-agent diff review and adds effort levels, `--fix`, `--comment`, and `/code-review ultra` for a cloud review. The `code-reviewer` agent and `/review-pr` (which drives it for a formal GitHub review decision) are unaffected +- **Commands** (`.claude/commands/code-review.md`): deleted, because `/code-review` is now a built-in Claude Code command and a project command of the same name shadows it — the built-in was absent from the session's skill list until this file was removed. The built-in supersedes the copy. The `code-reviewer` agent and `/review-pr` are unaffected ### Fixed -- **Hooks** (`session-start.sh`): `input=$(cat)` assigned the hook payload to a variable the script never read (shellcheck `SC2034`), found by the new CI workflow on its first run. Not a bug — the hook inspects the filesystem, not the payload — but the dead assignment made it look like the payload mattered. Replaced with `cat >/dev/null` plus a comment explaining that stdin is drained so Claude Code's write to the pipe always completes - -- **Hooks** (`protect-main.sh`): the broad `rm -rf` guard used `\b` (word-boundary) to close each dangerous target (`/`, `.`, `..`, `~`), which doesn't behave as a token boundary — it matches on any adjacent word character and doesn't match at all at end-of-string. Result: the guard silently let through the most common forms of the command (`rm -rf .`, `rm -rf ..`, `rm -rf /`, `rm -rf ~`, with no trailing space), while also incorrectly blocking legitimate specific targets like `rm -rf .git` or `rm -rf ~/tmp-dir`. Replaced the closing boundary with `($|\s)` so it matches the whole token instead. Verified against both dangerous and legitimate cases by invoking the hook directly with crafted input - -- **Settings** (`.claude/settings.json`): the `enforce-uv.sh`/`protect-main.sh` `PreToolUse` entries combined multiple patterns in one `if` string (e.g. `Bash(python *)|Bash(pytest *)|...`); the `if` field holds exactly one permission rule with no `||`/list syntax, so the condition never matched and both hooks silently stopped firing — including `protect-main.sh`'s guardrails against force-push, direct push to main, `git reset --hard`, and broad `rm -rf`. Split each pattern into its own hook handler entry (8 for `enforce-uv.sh`, 2 for `protect-main.sh`), per [code.claude.com/docs/en/hooks](https://code.claude.com/docs/en/hooks) - -- **README.md**: the subagents section claimed nesting goes "up to 5 levels deep". The real default is **3 layers** below the main conversation (raised from 1 in Claude Code v2.1.219); at the limit Claude Code withholds the `Agent` tool so the subagent does the work itself. Documented `CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH` as the way to change it - -- **README.md**: the `/orchestrate` row named the pipeline stages `planner → tdd → code-review → security`, which don't match any agent names. Corrected to the actual agents the command drives: `planner → tdd-guide → code-reviewer → security-reviewer` - -### Changed - -- **Skills** (`.claude/skills/`): re-synced the ten skills vendored from [anthropics/skills](https://github.com/anthropics/skills) — `claude-api`, `doc-coauthoring`, `docx`, `frontend-design`, `mcp-builder`, `pdf`, `pptx`, `skill-creator`, `webapp-testing`, `xlsx` — against upstream `f17010c`. They were copied on 2026-03-20 and never refreshed, so five months of upstream changes had accumulated: - - **`claude-api`** was the most consequential: it still told Claude to default to `claude-opus-4-6`, and `shared/models.md` knew nothing of Opus 5, Sonnet 5, or Fable 5 — a reference skill actively steering code toward superseded model IDs. Now defaults to `claude-opus-5`. Upstream also split the per-language docs into directories (`python/claude-api/*.md` in place of a single `python/claude-api.md`), replaced the `*/agent-sdk/` pages with `shared/agent-design.md`, and added the Managed Agents doc set, prompt caching, token counting, and model migration pages - - **`docx`, `pptx`, `xlsx`** picked up upstream's 2026-07-17 consolidation of the shared `scripts/office/` helpers, which carries security fixes, plus template-format support (`.dotx`, `.potx`, `.xltx`) now reflected in their trigger descriptions - - **`frontend-design`** was rewritten upstream from "production-grade interfaces" into visual-design direction (aesthetics, typography, avoiding templated defaults) - - The remaining skills changed only in `LICENSE.txt` (upstream filled in the copyright line) or not at all -- **README.md**: flagged which ten skills are vendored copies from upstream, warned that they don't self-update, and recorded the sync point (`f17010c`, 2026-08-13) so the next refresh has a baseline. Updated the `claude-api`, `docx`, `pptx`, `skill-creator`, and `frontend-design` rows to match their new upstream descriptions -- **README.md**: noted that Claude Code v2.1.200 renamed the `default` permission mode's **label** to "Manual" across the CLI, the VS Code and JetBrains extensions, and the desktop app, and accepts `"manual"` as an alias. The `"default"` value in `.claude/settings.json` stays canonical, so nothing needed migrating — but readers looking for "default" in the `Shift+Tab` cycle wouldn't find it -- **README.md**: replaced the short "many more events" aside in the hooks section with the current count (32) and a list of the events most useful for extending this setup — `PostToolBatch`, `PostToolUseFailure`, `SubagentStart`/`SubagentStop`, `InstructionsLoaded`, `PermissionRequest`/`PermissionDenied`, `ConfigChange`, `FileChanged` -- **Settings** (`.claude/settings.json`): added `permissions.defaultMode: "default"` explicitly — same behavior as the implicit default, but now visible and easy to customize -- **.env.example**: added `CLAUDE_CODE_DISABLE_BUNDLED_SKILLS="0"` (explicit default; set to `1` to hide Claude Code's own bundled skills/workflows, project `.claude/skills/` is unaffected) -- **README.md**: documented `permissions.defaultMode`, and noted `permissions.disableAutoMode`/`language` as available but intentionally unset (both lack a neutral value that preserves default behavior) -- **Settings** (`.claude/settings.json`): `fallbackModel` refreshed from the stale `claude-sonnet-4-6`/`claude-haiku-4-5` IDs to the current `claude-sonnet-5`/`claude-haiku-4-5-20251001` -- **Hooks** (`enforce-uv.sh`, `protect-main.sh`): added `if` conditions to their `PreToolUse` entries so they only spawn on matching Bash commands (Python/pip tooling, git/rm) instead of every Bash call -- **README.md**: synced the `fallbackModel` example; documented the hook `if` conditional field; updated `CLAUDE_CODE_NO_FLICKER` (fullscreen rendering is now default-on, no longer a research preview) and `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS` (implicit team model replaces `TeamCreate`/`TeamDelete`); noted subagents now run in the background by default; bumped the RTK reference to v0.43.0 and mentioned `rtk gain` -- **.env.example**: `CLAUDE_CODE_NO_FLICKER` default flipped to `1` to match the new Claude Code default +- **Hooks** (`session-start.sh`): `input=$(cat)` assigned the payload to a variable the script never read (shellcheck `SC2034`), caught by the new CI on its first run. Not a bug — the hook inspects the filesystem, not the payload — but the dead assignment implied otherwise. Replaced with `cat >/dev/null` and a comment noting stdin is drained so Claude Code's write to the pipe completes +- **Hooks** (`protect-main.sh`): the broad `rm -rf` guard used `\b` to close each dangerous target (`/`, `.`, `..`, `~`), which does not behave as a token boundary — it matches on any adjacent word character and never matches at end-of-string. The guard let through the most common forms (`rm -rf .`, `rm -rf ..`, `rm -rf /`, `rm -rf ~`) while incorrectly blocking legitimate targets like `rm -rf .git` and `rm -rf ~/tmp-dir`. Replaced with `($|\s)` so it matches the whole token +- **Settings** (`.claude/settings.json`): the `enforce-uv.sh`/`protect-main.sh` `PreToolUse` entries combined multiple patterns in one `if` string; the field holds exactly one permission rule with no `||` or list syntax, so the condition never matched and both hooks silently stopped firing — taking `protect-main.sh`'s force-push, push-to-main, `git reset --hard` and `rm -rf` guards with them. Split each pattern into its own handler entry (8 for `enforce-uv.sh`, 2 for `protect-main.sh`) +- **README.md**: the subagents section claimed nesting goes "up to 5 levels deep"; the default is 3 layers below the main conversation. Documented `CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH` as the way to change it +- **README.md**: the `/orchestrate` row named stages (`planner → tdd → code-review → security`) matching no agent names. Corrected to the agents the command drives: `planner → tdd-guide → code-reviewer → security-reviewer` +- **README.md**: the optional-dependency table credited `jq` to three hooks; all six parse their payload with it, so a partial install would break the other three +- **README.md**: the project tree and the CI section described the workflow as lint plus configuration validation, omitting the shellcheck and pytest steps, and the ruff scope line omitted `tests/` ## [1.3.0] - 2026-06-14 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9507c38..be5993c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,10 +22,11 @@ Open a [feature request](https://github.com/skateddu/claude-code-python-setup/is 1. Fork the repository 2. Create a branch from `main`: `git checkout -b feature/your-change` 3. Make your changes -4. Run the verification checks: +4. Run the verification checks — these are the same four the CI runs, so passing here means passing there: ```bash - uv run ruff check . && uv run ruff format --check . && uv run pytest -x + uv run ruff check . && uv run ruff format --check . && uv run python scripts/validate_config.py && uv run pytest ``` + CI additionally runs `shellcheck --severity=warning .claude/hooks/*.sh`, which needs [shellcheck](https://github.com/koalaman/shellcheck#installing) locally. 5. Commit using [Conventional Commits](https://www.conventionalcommits.org/) format: ``` feat(rules): add new testing pattern for async fixtures @@ -43,9 +44,11 @@ cd claude-code-python-setup uv sync # Verify everything works -uv run ruff check . && uv run ruff format --check . +uv run ruff check . && uv run ruff format --check . && uv run python scripts/validate_config.py && uv run pytest ``` +The hook tests shell out to the scripts in `.claude/hooks/`, so they need `bash` and `jq` on your PATH. Without them the suite skips rather than fails. + ## Coding Standards - Follow the rules defined in `.claude/rules/` diff --git a/README.md b/README.md index 36428e5..3ebabc1 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Some components require additional tools. Install only what you need: | **Python >= 3.10 + uv** | postgres, docker MCP servers | [docs.astral.sh/uv](https://docs.astral.sh/uv/getting-started/installation/) | | **Docker** | docker MCP server | [docker.com](https://www.docker.com/get-started/) | | **PostgreSQL** | postgres MCP server | Running instance (local or remote) | -| **jq** | hooks (enforce-uv, protect-main, auto-lint) | [jqlang.github.io/jq](https://jqlang.github.io/jq/download/) | +| **jq** | all six hooks — each one parses its payload and builds its response with it | [jqlang.github.io/jq](https://jqlang.github.io/jq/download/) | | **bubblewrap + socat** | [Bash sandbox](#bash-sandbox-opt-in-not-enabled-here) on Linux/WSL2 only (macOS needs nothing; native Windows unsupported) | your package manager | | **[RTK](https://github.com/rtk-ai/rtk)** | token optimization (recommended) | [install guide](https://github.com/rtk-ai/rtk#installation) | @@ -143,7 +143,7 @@ claude-code-python-setup/ │ └── xlsx/ ├── .github/ │ └── workflows/ -│ └── ci.yml # Lint + configuration validation on every PR +│ └── ci.yml # Lint, config validation, shellcheck, hook tests ├── mcp_config/ │ ├── linux_mac.mcp.json # MCP server config (Linux/Mac) │ └── windows.mcp.json # MCP server config (Windows) @@ -218,7 +218,7 @@ setx CLAUDE_AUTOCOMPACT_PCT_OVERRIDE 85 | `POSTGRES_*` | postgres MCP server | see `.env.example` | Database connection parameters | | `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE` | Claude Code | `95` | Context % threshold that triggers auto-compaction (lower = compacts earlier, reduces response time) | | `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS` | Claude Code | `0` (disabled) | Set to `1` to enable Agent Teams: spawning a teammate via the Agent tool's `name` parameter implicitly forms a team for the session (no `TeamCreate`/`TeamDelete` setup needed) ([docs](https://code.claude.com/docs/en/agent-teams)) | -| `CLAUDE_CODE_NO_FLICKER` | Claude Code | `1` (enabled) | Fullscreen rendering (flicker-free display, flat memory usage, mouse support) is now the default; set to `0` to opt back into the classic renderer, or use `"tui": "fullscreen"` in `settings.json` ([docs](https://code.claude.com/docs/en/fullscreen)) | +| `CLAUDE_CODE_NO_FLICKER` | Claude Code | `1` (enabled) | Fullscreen rendering (flicker-free display, flat memory usage, mouse support) is the default; set to `0` for the classic renderer, or use `"tui": "fullscreen"` in `settings.json` ([docs](https://code.claude.com/docs/en/fullscreen)) | | `CLAUDE_CODE_DISABLE_BUNDLED_SKILLS` | Claude Code | `0` (enabled) | Set to `1` to hide the skills and workflows bundled with Claude Code itself (e.g. `/init`, `/security-review`); plugin skills and this project's own `.claude/skills/` are unaffected. Equivalent to `"disableBundledSkills": true` in `settings.json` ([docs](https://code.claude.com/docs/en/settings)) | | `BASH_DEFAULT_TIMEOUT_MS` | Claude Code | `120000` (2 min) | Default timeout for Bash commands. Raise it if your test suite regularly runs longer than two minutes, otherwise `uv run pytest` gets killed mid-run | | `BASH_MAX_OUTPUT_LENGTH` | Claude Code | `30000` (max `150000`) | Characters of command output Claude reads back. Raise it when verbose `pytest -v` output gets truncated before the failure summary | @@ -359,7 +359,7 @@ These rules are enforced at the system level — Claude cannot bypass them regar **Default permission mode** — `permissions.defaultMode` is set explicitly to `"default"` (prompt on first use of each tool) so the behavior is visible and easy to change, rather than relying on the implicit default. Other values: `"plan"` (read-only, no modifications), `"acceptEdits"` (auto-accepts file edits), `"bypassPermissions"` (skips all prompts — isolated environments only), `"dontAsk"` (auto-denies unless pre-approved), `"auto"` (auto-approves with background safety checks). -> Since Claude Code v2.1.200 the `default` mode is **labeled "Manual"** in the CLI, the VS Code and JetBrains extensions, and the desktop app — look for "Manual", not "default", in the `Shift+Tab` mode cycle. The `"default"` value stays canonical and needs no migration; `"manual"` is accepted as an alias. +> The label and the value differ: in the `Shift+Tab` cycle this mode appears as **"Manual"**, not "default" — in the CLI, the VS Code and JetBrains extensions, and the desktop app. `"default"` is the canonical value in `settings.json`; `"manual"` is accepted as an alias on Claude Code v2.1.200+. Two related settings are intentionally **not** set here, since they have no neutral value that preserves default behavior while being explicit — adding them would itself be a behavior change: @@ -426,7 +426,7 @@ Key characteristics: - **Isolated context**: each agent runs in its own context window, keeping verbose output out of the main conversation - **Configurable tools and model**: agents can restrict tool access and use a different model (e.g., Haiku for speed) - **Nesting**: subagents can spawn their own subagents, up to 3 layers below the main conversation by default; at the limit Claude Code withholds the `Agent` tool so the subagent does the work itself. Change the limit with `CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH` -- **Background by default**: subagents now run in the background while you keep working, and notify on completion or when they need input +- **Background by default**: subagents run in the background while you keep working, and notify on completion or when they need input > Full documentation: [code.claude.com/docs/en/sub-agents](https://code.claude.com/docs/en/sub-agents) @@ -548,11 +548,11 @@ The script is written in Python and works cross-platform: Windows, macOS, and Li ## Continuous Integration -`.github/workflows/ci.yml` runs on every pull request and on pushes to `main`. It lints with ruff and validates that the configuration this template ships is internally coherent — the class of breakage that would otherwise reach whoever copies the `.claude/` folder. +`.github/workflows/ci.yml` runs on every pull request and on pushes to `main`. It lints, checks that the configuration this template ships is internally coherent, and runs the hook tests — the kinds of breakage that would otherwise reach whoever copies the `.claude/` folder. | Check | Catches | |-------|---------| -| `ruff check` / `ruff format --check` | Lint and formatting on `.claude/statusline.py` and `scripts/` (`src/**/*.py` covers projects built from this template) | +| `ruff check` / `ruff format --check` | Lint and formatting on `.claude/statusline.py`, `scripts/` and `tests/` (`src/**/*.py` is in scope for projects built from this template) | | `scripts/validate_config.py` | Unparseable `settings.json` or MCP config; hooks pointing at scripts that no longer exist; `CLAUDE.md` `@`-imports that don't resolve; skills whose `SKILL.md` lost its `name`/`description` frontmatter | | `shellcheck --severity=warning` | Shell quoting and syntax bugs in the hook scripts | | `pytest` | Hooks reaching the **wrong decision** — see below | @@ -565,9 +565,9 @@ uv run ruff check . && uv run ruff format --check . && uv run python scripts/val ### Hook tests (`tests/`) -The hooks are the only enforced guardrails in this setup, and every past bug in them has been a **logic** error rather than a shell error — a condition that silently disabled a hook, a regex boundary that let `rm -rf /` through while blocking `rm -rf .git`. shellcheck cannot see either. +The hooks are the only enforced guardrails in this setup, and the faults that matter in them are **logic** errors rather than shell errors: a script can be syntactically clean, correctly wired, and still reach the wrong decision — or stop firing altogether. shellcheck sees none of that. -So each test runs the real script in a subprocess with a crafted payload and asserts on the decision it emits. Nothing is mocked, because "the hook stopped firing" is precisely the regression worth catching. +So each test runs the real script in a subprocess with a crafted payload and asserts on the decision it emits. Nothing is mocked, because "the hook stopped firing" is precisely the regression worth catching, and a mock cannot fail that way. | File | Asserts | |------|---------| @@ -583,7 +583,7 @@ Two gaps are deliberate and worth knowing: - **`verify.sh`'s ruff/pytest execution path is not covered.** Running it from inside the suite would invoke pytest recursively, and a throwaway uv project would need a network install on every CI run. Only its guard clauses are tested. - **`auto-lint.sh`'s formatting path is not covered**, because whether ruff acts on a given file depends on the surrounding project's `include` configuration. Only the conditions under which the hook must do nothing are tested. -Neither the tests nor `validate_config.py` check the **hook wiring** in `settings.json` — that a hook's `matcher` and `if` condition actually route the events you expect. That wiring has broken before, and it remains verifiable only by running Claude Code. +Neither the tests nor `validate_config.py` check the **hook wiring** in `settings.json` — that a hook's `matcher` and `if` condition actually route the events you expect. A hook can be correct, referenced, and still never fire. That wiring is verifiable only by running Claude Code. ## Contributing