From 735ba2c8d942a058a81d7fda0e9236673a9622ac Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Tue, 5 May 2026 23:25:03 +0200 Subject: [PATCH] docs: add CLAUDE_CODE_TOOLING.md and cross-reference from CLAUDE.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documents the deterministic guard-rail layer: hooks reference, crewli-reviewer subagent usage, three slash commands, how to test hooks, how to disable temporarily, and the binding design principle (settings/hooks deterministic, CLAUDE.md advisory — never duplicate). --- CLAUDE.md | 2 + dev-docs/CLAUDE_CODE_TOOLING.md | 192 ++++++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 dev-docs/CLAUDE_CODE_TOOLING.md diff --git a/CLAUDE.md b/CLAUDE.md index 50627bfc..905f3b7f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,5 +1,7 @@ # Crewli — Claude Code Instructions +> See `dev-docs/CLAUDE_CODE_TOOLING.md` for the deterministic guard-rail layer (hooks, subagent, slash commands). + ## Project context Crewli is a multi-tenant SaaS platform for event and festival management. diff --git a/dev-docs/CLAUDE_CODE_TOOLING.md b/dev-docs/CLAUDE_CODE_TOOLING.md new file mode 100644 index 00000000..49a72afd --- /dev/null +++ b/dev-docs/CLAUDE_CODE_TOOLING.md @@ -0,0 +1,192 @@ +# Claude Code tooling — `.claude/` reference + +## 1. Purpose + +`/CLAUDE.md` is *advisory*: it tells Claude what to do. Claude is good +about following it, but advisory rules fail open — when Claude misses +a line, nothing catches it. Crewli runs a parallel *deterministic* +layer in `.claude/` that fires whether or not Claude reads it: hooks +on tool calls, a code-review subagent, and slash commands for the +procedural shortcuts that come up every sprint. + +The two layers are not redundant. They have different jobs. + +> **Binding principle:** Would Claude make a mistake without this +> rule? If no, delete it. If yes, hook it. CLAUDE.md is for +> understanding; `.claude/` is for guarantees. + +A rule belongs in `settings.json` / hooks if and only if Claude would +make a mistake without it. Otherwise the rule belongs nowhere — not +in CLAUDE.md, not in a hook. Don't double up: a hook rule that's also +documented in CLAUDE.md just rots when you change one and forget the +other. + +## 2. Layout + +``` +.claude/ +├── settings.json # Hook registry (committed) +├── settings.local.json # Per-user overrides (gitignored) +├── agents/ +│ └── crewli-reviewer.md # Code-review subagent +├── commands/ +│ ├── sprint-status.md # /sprint-status +│ ├── review-multitenancy.md # /review-multitenancy +│ └── sync-docs.md # /sync-docs +└── hooks/ + ├── protect-files.sh # PreToolUse Edit/Write + ├── block-dangerous-bash.sh # PreToolUse Bash + ├── post-edit-pint.sh # PostToolUse Edit/Write — PHP + ├── post-edit-eslint.sh # PostToolUse Edit/Write — JS/TS/Vue + └── inject-sprint-context.sh # SessionStart compact +``` + +Everything except `settings.local.json` is checked in. + +## 3. Hooks reference + +| Event | Matcher | Script | Behaviour | Fail mode | +|---|---|---|---|---| +| PreToolUse | `Edit\|Write\|MultiEdit` | `protect-files.sh` | Blocks edits to secrets, lock files, default migrations, the deleted `apps/admin/`, `.claude/` itself, and `dev-docs/SCHEMA.md`. | Exit 2 with reason on stderr. | +| PreToolUse | `Bash` | `block-dangerous-bash.sh` | Blocks `git reset --hard`, force pushes, blanket dependency updates, and database wipes that aren't scoped to `--env=testing`. | Exit 2 with reason on stderr. | +| PostToolUse | `Edit\|Write\|MultiEdit` | `post-edit-pint.sh` | Runs `vendor/bin/pint --dirty` from `api/` after any `.php` edit. | Exit 0 silently — formatting failures never block. | +| PostToolUse | `Edit\|Write\|MultiEdit` | `post-edit-eslint.sh` | Runs `pnpm eslint --fix` inside the matching SPA dir for `.vue/.ts/.tsx/.js` files under `apps/app/` or `apps/portal/`. | Exit 0 silently. | +| SessionStart | `compact` | `inject-sprint-context.sh` | Prints branch, last 10 commits, and the top of `BACKLOG.md` so Claude resumes with sprint context after auto-compaction. | Exit 0; output is appended to context. | + +Every script: + +- Sets `set -euo pipefail`. +- Reads JSON from stdin via `jq`. +- Resolves paths through `$CLAUDE_PROJECT_DIR`. +- Completes in well under 500 ms for typical inputs. + +PreToolUse hooks signal *block* by exiting `2` (never `1` — `1` is a +hook misconfiguration, not a policy denial). PostToolUse hooks always +exit `0`; a formatter that fails should not stop the agent. + +## 4. Subagent: `crewli-reviewer` + +Lives at `.claude/agents/crewli-reviewer.md`. Isolated context, +review-only — it has Read / Grep / Glob / Bash but no Edit. Designed +for a single pass against the zero-compromise principles in CLAUDE.md +and the schema rules in `dev-docs/SCHEMA.md`. + +### When to invoke + +After any non-trivial implementation, before committing: + +> Use the crewli-reviewer subagent on the changes since HEAD~1. + +### Report shape + +Three sections, every finding cited as `path/to/file.php:LINE`: + +- **MUST FIX** — blocking, violates a zero-compromise principle. +- **SHOULD FIX** — non-blocking but clearly correct improvement. +- **CONSIDER** — judgment call surfaced for Bert. + +If the diff is clean, the agent outputs `No issues found against the +zero-compromise principles.` and stops. + +### Extending the system prompt + +Edit `crewli-reviewer.md`. The frontmatter (`name`, `description`, +`tools`, `model`) is the discovery contract — keep `tools` minimal so +the reviewer can't accidentally patch code. Add new checklist items +under the existing checklist sections; if a recurring pattern of +review findings emerges, promote it to the *six most-missed gaps* +list at the bottom of the prompt. + +Don't move review checks into `CLAUDE.md` thinking it's redundant — +the subagent runs in an isolated context that may not have read +`CLAUDE.md` yet, so the prompt has to be self-contained. + +## 5. Slash commands + +| Command | Description | Example | +|---|---|---| +| `/sprint-status` | 5–10 line summary: current branch, last completed work package, uncommitted work, next BACKLOG item. | `/sprint-status` | +| `/review-multitenancy ` | Reads model + migration + policy + tests for `` and reports PASS/FAIL/N/A on the multi-tenancy checklist. Ends with `READY` or `NEEDS WORK`. | `/review-multitenancy Shift` | +| `/sync-docs` | Runs `npm run sync:docs`, prints the manifest's Git SHA + generation timestamp, and reminds you to upload `.claude-sync/` to Project Knowledge. | `/sync-docs` | + +Slash commands live in `.claude/commands/.md`. Frontmatter +declares `description`, optional `argument-hint`, and an +`allowed-tools` allowlist (least-privilege — `/sync-docs` only allows +`Bash(npm:*)` and `Read`, not arbitrary Bash). + +## 6. Testing a hook + +Each hook reads a single JSON object on stdin. Test by piping a +fixture and inspecting the exit code and stderr: + +```bash +# protect-files — should BLOCK +echo '{"tool_input":{"file_path":".env"}}' | bash .claude/hooks/protect-files.sh; echo "exit=$?" + +# protect-files — should ALLOW +echo '{"tool_input":{"file_path":"app/Models/Event.php"}}' | bash .claude/hooks/protect-files.sh; echo "exit=$?" + +# block-dangerous-bash — should BLOCK +echo '{"tool_input":{"command":"git push --force origin main"}}' | bash .claude/hooks/block-dangerous-bash.sh; echo "exit=$?" + +# block-dangerous-bash — should ALLOW (testing scope) +echo '{"tool_input":{"command":"php artisan migrate:fresh --env=testing --seed"}}' | bash .claude/hooks/block-dangerous-bash.sh; echo "exit=$?" + +# post-edit-pint — no-op on non-PHP +echo '{"tool_input":{"file_path":"README.md"}}' | bash .claude/hooks/post-edit-pint.sh; echo "exit=$?" + +# post-edit-eslint — no-op on PHP +echo '{"tool_input":{"file_path":"api/app/Models/Event.php"}}' | bash .claude/hooks/post-edit-eslint.sh; echo "exit=$?" + +# inject-sprint-context — full output +bash .claude/hooks/inject-sprint-context.sh +``` + +Block scenarios should print a reason on stderr and exit `2`; allow +scenarios should be silent and exit `0`. + +## 7. Disabling temporarily + +Set `"disableAllHooks": true` at the top level of `.claude/settings.json`: + +```json +{ + "disableAllHooks": true, + "hooks": { ... } +} +``` + +Document *why* in the commit message and revert before pushing the +branch you're working on. The hooks exist because they catch real +mistakes; turning them off is a knowingly-temporary state, not a +preference. + +For per-user overrides without touching the committed file, use +`.claude/settings.local.json` (gitignored). + +## 8. Adding a new hook + +Checklist: + +- [ ] Script completes in well under 500 ms (a slow hook makes every + tool call slower). +- [ ] PreToolUse: exit `2` to block (NEVER `1` — `1` reads as + misconfiguration). PostToolUse: always exit `0`. +- [ ] `set -euo pipefail` at the top. +- [ ] JSON parsed via `jq` from stdin (no shelling out to grep over + the whole payload). +- [ ] Path resolution via `$CLAUDE_PROJECT_DIR`, never `$PWD`. +- [ ] Test fixture in this document's section 6 covers the new + pattern. +- [ ] Entry added to the hooks reference table in section 3. +- [ ] `chmod +x` set, committed. + +## 9. Design principle (verbatim) + +Settings and hooks are deterministic; CLAUDE.md is advisory. They +serve different jobs and should never duplicate. If a rule is +hookable, hook it and *delete* it from CLAUDE.md — keeping both is +the worst of both worlds (the agent reads the rule, the hook fires +the rule, and when they diverge nobody knows which is right). +CLAUDE.md is for understanding the project; `.claude/` is for +catching the mistakes that happen anyway.