Files
crewli/dev-docs/CLAUDE_CODE_TOOLING.md
bert.hausmans bea66a58e6 chore(docs): purge apps/portal mention from CLAUDE_CODE_TOOLING.md
Single-line fix in the hooks reference table. The post-edit-eslint
hook used to scope to apps/app/ or apps/portal/; post-WS-3 there's
only apps/app/.

Code change in the hook script itself lands in the next commit.
2026-05-06 01:51:37 +02:00

8.4 KiB
Raw Permalink Blame History

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 <Model>
│   └── 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, database wipes that aren't scoped to --env=testing, and rm -rf on absolute paths outside /tmp, /var/folders, and $HOME. 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 apps/app/ for .vue/.ts/.tsx/.js files. 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 11 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 510 line summary: current branch, last completed work package, uncommitted work, next BACKLOG item. /sprint-status
/review-multitenancy <Model> Reads model + migration + policy + tests for <Model> 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/<name>.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:

# 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:

{
  "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 11 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.