Adds crewli-architect, backend/frontend-implementer, test-writer subagents, the /build-module orchestrator command, the PR merge-gate template, and a permissions allow-list in settings.json. Documents the layer as CLAUDE_CODE_TOOLING.md section 10. Implementer Edit/Write is allow-listed; git push deliberately omitted so merge/push stay human. Co-Authored-By: Claude <noreply@anthropic.com>
274 lines
12 KiB
Markdown
274 lines
12 KiB
Markdown
# 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 `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 <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:
|
||
|
||
```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.
|
||
|
||
## 10. Multi-agent build pipeline
|
||
|
||
The `.claude/` layer now includes an orchestrated build pipeline on top
|
||
of the deterministic hooks and the review subagent. It automates the
|
||
work Bert previously did by hand (prompt authoring, dispatch, gate
|
||
assembly) while keeping the two irreversible decisions human.
|
||
|
||
### 10.1 Validated against
|
||
|
||
- Claude Code version: **<fill in `claude --version` at setup>**
|
||
- Re-verify after every `claude update`: the Agent-tool name, the
|
||
`isolation: worktree` field, and the subagent permission model have
|
||
all shifted in point releases. After an update, re-run the §6 hook
|
||
smoke-tests and one architect dry-run before trusting the chain.
|
||
|
||
### 10.2 The agents
|
||
|
||
| Agent | Model | Tools | Role |
|
||
|---|---|---|---|
|
||
| `crewli-architect` | opus | Read, Grep, Glob, Bash | Drift-check, audit, decompose, emit DECISION BRIEF. Plans only — never writes code. |
|
||
| `backend-implementer` | sonnet | + Edit, Write (worktree) | One bounded backend subtask per the approved plan. |
|
||
| `frontend-implementer` | sonnet | + Edit, Write (worktree) | One bounded `apps/app/` subtask. |
|
||
| `test-writer` | sonnet | + Edit, Write (worktree) | PHPUnit + Vitest tests; never weakens a test to pass. |
|
||
| `crewli-reviewer` | opus | Read, Grep, Glob (read-only) | Zero-compromise review; emits `REVIEW VERDICT: PASS\|BLOCK`. |
|
||
|
||
Implementer prompts are deliberately thin: they encode only what an
|
||
agent would get WRONG without the instruction, and lean on CLAUDE.md +
|
||
SCHEMA.md for the rest. They do NOT duplicate hookable rules (pint,
|
||
eslint, protect-files, block-dangerous-bash already fire on every tool
|
||
call). This is the §1 binding principle applied to agents.
|
||
|
||
### 10.3 The orchestrator
|
||
|
||
`/build-module <task>` runs in the MAIN session (subagents can't spawn
|
||
subagents). Five phases:
|
||
|
||
0. Branch off main (always Phase 0).
|
||
1. Architect -> DECISION BRIEF -> **HUMAN GATE 1** (approve/adjust/reject).
|
||
2. Dispatch implementers + test-writer in dependency order; parallel-
|
||
safe subtasks run as background subagents with worktree isolation.
|
||
3. Reviewer gate; BLOCK loops back to the implementer without bothering
|
||
the human; PASS proceeds.
|
||
4. Assemble `pr-merge-gate.md` with real signals -> **HUMAN GATE 2**
|
||
(reply `merge`). A red signal never reaches the human.
|
||
5. Post-merge: sync-docs reminder; branch cleanup ONLY after merge
|
||
verification (the D1 near-miss rule).
|
||
|
||
### 10.4 The two human gates
|
||
|
||
Both gates are designed to reduce to a single glance + one word:
|
||
|
||
- **Gate 1** (decomposition): the architect surfaces its own risk flags
|
||
and open questions at the top of the brief, so Bert weighs only the
|
||
flagged points, not the whole plan. Reply `approve`.
|
||
- **Gate 2** (merge): every signal is pre-verified green before the gate
|
||
is shown; a red signal returns the PR to the implementer instead.
|
||
Bert performs the `--no-ff` merge + push manually. Reply `merge`.
|
||
|
||
`git push` is intentionally OFF the settings.json allow-list, so the
|
||
"merge & push stay human" rule is enforced at the permission layer.
|
||
|
||
### 10.5 Permissions interaction
|
||
|
||
Subagents can't answer "ask" prompts (an asked tool is auto-denied), so
|
||
implementer Edit/Write/Bash are allow-listed in settings.json §permissions.
|
||
The PreToolUse hooks still fire and block the dangerous subset. Allow
|
||
broadly; block narrowly via hooks. Never add `git push` to the allow-list.
|
||
|
||
### 10.6 Files
|
||
|
||
```
|
||
.claude/agents/crewli-architect.md
|
||
.claude/agents/backend-implementer.md
|
||
.claude/agents/frontend-implementer.md
|
||
.claude/agents/test-writer.md
|
||
.claude/agents/crewli-reviewer.md (existing + verdict-line block)
|
||
.claude/commands/build-module.md
|
||
.claude/templates/pr-merge-gate.md
|
||
.claude/settings.json (existing hooks + new permissions)
|
||
```
|