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.
193 lines
8.4 KiB
Markdown
193 lines
8.4 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.
|