From 706057a4bd0bf16d678bc2a8b60173356ad9344a Mon Sep 17 00:00:00 2001 From: skateddu Date: Thu, 13 Aug 2026 15:33:40 +0200 Subject: [PATCH] docs: align README, CHANGELOG and CONTRIBUTING with actual state The README had drifted into narrating change ("is now the default", "has broken before") where it should describe the current state, and carried four claims that no longer matched the repo: jq credited to three of six hooks, a CI description predating the shellcheck and pytest steps, and a ruff scope line missing tests/. CONTRIBUTING told contributors to verify with ruff alone while CI runs four checks, so it was possible to be green locally and red in CI. The CHANGELOG's Unreleased section restated README content at length, which would silently go stale, listed its sections in an order matching neither Keep a Changelog nor its own released sections, and described a CI workflow without the pytest step added later in the same cycle. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 68 ++++++++++++++++++++++++------------------------- CONTRIBUTING.md | 9 ++++--- README.md | 20 +++++++-------- 3 files changed, 50 insertions(+), 47 deletions(-) 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