Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 34 additions & 34 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading