Claude Code & AI agents
WaveHouse ships minimal team-wide Claude Code™ configuration. The repo commits only what’s distinctly useful to every contributor; cosmetic and personal choices stay at the user level.
If you’re new to Claude Code itself, the official docs cover the basics. This page is WaveHouse-specific.
Quick setup
Section titled “Quick setup”-
Install Claude Code: follow the official install guide. On macOS:
brew install --cask claude-code. -
Authenticate: log in to your Max subscription — Claude Code prompts you on first run.
-
Bootstrap the repo:
make tools. This installs Go tools, pnpm deps, and also configures git hooks (git config core.hooksPath .githooks). Without this step, the team’s pre-commit / pre-push gates won’t fire. -
Optional — worktrunk: install worktrunk for parallel-agent worktree management. The team’s project hooks live in
.config/wt.toml.
How enforcement is layered
Section titled “How enforcement is layered”| Layer | Lives in | Applies to | Purpose |
|---|---|---|---|
| Git hooks | .githooks/ (installed by make tools) | Humans + Claude™ uniformly | Hard enforcement: make verify on commit; before push, make ci for a code change or make verify for a docs-only one (classifier-gated) |
| Claude Code agent gate | .claude/hooks/agent-bash-gate.sh (PreToolUse Bash) + .claude/settings.json deny rules | Agents only | Catches accidental violations of Agent PR Discipline: drafts only, no human reviewer adds, a marker (from a review or a logged skip) from every reviewer in scripts/pre-push-reviewers.sh required for the commit each refspec publishes when main doesn’t have it yet, judged in the worktree the push runs in; PR title linted via scripts/lint-pr-title.sh; the gh api REST and GraphQL forms of these, and of a merge, checked too. Plain text checks: only plain forms are caught |
| Claude Code ergonomic hooks | .claude/hooks/gofumpt-on-save.sh + .claude/hooks/markdown-on-save.sh (PostToolUse Edit/Write/MultiEdit), .claude/hooks/review-marker.sh (SubagentStop) | Claude only | gofumpt / markdown-on-save: auto-format on file edits (humans get this from their IDE). review-marker: on a reviewer’s VERDICT: ship_it, writes that reviewer’s tmp/<name>-passed-<sha> marker for the commit its REVIEWED: line names |
| Claude Code skills / agents / commands | .claude/skills/, .claude/agents/, .claude/commands/ | Claude only (when relevant) | Workflow guidance and on-demand helpers — not gates |
Git hooks are the source of truth for “must pass before merge.” .claude/ layers agent-specific gates and ergonomic hooks on top; it doesn’t substitute for the universal gates.
Git hooks (.githooks/)
Section titled “Git hooks (.githooks/)”Two scripts, both committed to the repo:
| Hook | Behavior |
|---|---|
pre-commit | Runs make verify (the static checks listed in Development → Makefile Targets, ~30s) — blocks on failure. Skipped if make ci or make verify already ran for the current tree state (cached via scripts/ci-marker.sh). A worktree with no Makefile has nothing to verify and commits; a missing or failing scripts/ci-marker.sh is reported and make verify runs. |
pre-push | Scales the bar to the change set (same classifier CI uses, scripts/classify-paths.sh): a code change requires the make ci marker (tmp/ci-passed-tree-<TREE-sha>, from make ci or make ci-remote); a docs/prose-only push requires only the make verify marker (tmp/verify-passed-tree-<TREE-sha>) — CI classifies a PR by its full file list, so it skips the Go/SDK suites only when the whole PR is docs/prose. A tag of a commit origin/main already contains (a release tag) needs no marker. Blocks if the required marker is absent. Fail-closed: an unclassifiable push falls back to requiring make ci. A pushed commit with no Makefile goes through, having no code to validate; one with a Makefile, pushed from a worktree without scripts/ci-marker.sh, is blocked. |
--no-verify is for intentional WIP / draft pushes. Agents should not use it — policy in AGENTS.md §“Agent PR Discipline”, not regex-enforced.
Both markers are tree-keyed so commit-then-push works without a re-run when the tree is unchanged. make ci / make verify skip the marker write when $CI is set (CI runners don’t push). Shared logic lives in scripts/ci-marker.sh.
make tools sets core.hooksPath to the relative .githooks, which git resolves in each worktree: each worktree runs its own branch’s hooks, and one whose branch has no .githooks/ runs none. An absolute core.hooksPath runs one checkout’s hooks in every worktree, which is why the hooks handle a worktree with no code. make test-githooks (part of make verify) commits and pushes through the real hooks from scratch worktrees with and without code.
What’s in .claude/ and .config/
Section titled “What’s in .claude/ and .config/”| Path | Purpose |
|---|---|
.claude/settings.json | Team-wide: deny permissions (force-push / git reset —hard / filter-branch / update-ref -d, gh pr merge / ready / approve / request-changes, gh repo/release delete, gh secret delete, gh workflow disable, rm -rf / sudo rm), all four hooks wired, each through a guard that runs its script when it’s there and fails closed when it isn’t (see When a hook script is missing) |
.claude/hooks/gofumpt-on-save.sh | PostToolUse Edit/Write/MultiEdit: auto-formats .go files |
.claude/hooks/markdown-on-save.sh | PostToolUse Edit/Write/MultiEdit: repairs MDX fence structure in .mdx, applies markdownlint --fix (WH001 unwrapping) in .md only, and runs misspell over docs prose in both |
.claude/hooks/agent-bash-gate.sh | PreToolUse Bash: catches accidental Agent PR Discipline violations with plain text checks over the whole command line. Blocks gh pr create / new without --draft, a malformed literal PR title (scripts/lint-pr-title.sh), reviewer or assignee adds, approvals and change requests, and the gh api REST and GraphQL forms of those, of ready-for-review and of a merge. A git push must be a plain git [-C <worktree>] push … on its own (optionally 2>&1 and one | filter), and the commit each refspec publishes needs a marker (from a review or a logged skip) from every reviewer in scripts/pre-push-reviewers.sh when origin/main (local main if there’s no origin/main) doesn’t have it yet; a marker counts from any worktree of the repository. Because it matches text rather than parsing the shell, a command that only mentions one of these (a --body naming a guarded mutation, a commit message containing git push) is blocked too: put long text in a file (--body-file, git commit -F <file>, gh api -F body=@file). Only plain forms are caught; any other form is still against the rules |
.claude/hooks/review-marker.sh | SubagentStop: on a reviewer’s VERDICT: ship_it, writes its tmp/<name>-passed-<sha> marker for the commit named on the report’s REVIEWED: line, into the worktree on that commit, and only if that worktree’s HEAD stayed on it for the whole review (its reflog against the subagent transcript’s first timestamp). The reviewer set comes from scripts/pre-push-reviewers.sh; filters by agent_type in-script. Reads the report from .last_assistant_message, or, when that has no VERDICT: line or says ship_it without REVIEWED:, from the hand-back message in the subagent’s transcript; a final iterate or block stands. An abbreviated sha counts if it names exactly one commit. Logs every decision to tmp/review-marker.log |
.claude/hooks/review-gate.test.sh | Test for the two review hooks: feeds them synthetic events in a scratch repository with sibling worktrees (make test-review-gate, part of make verify) |
.claude/hooks/hook-commands.test.sh | Test for the hook commands in .claude/settings.json: runs each one under sh -c as Claude Code does, with its script present, missing and not executable (make test-hook-commands, part of make verify) |
.claude/commands/cover.md | /cover [suite] — suite dispatch + coverage threshold analysis |
.claude/commands/docs-review.md | /docs-review [path|all] — launches the docs-reviewer subagent. No-arg = the gating pre-push docs review; a path/all = advisory |
.claude/commands/prepush.md | /prepush [all] — the pre-push gate: reads scripts/pre-push-reviewers.sh, runs the reviewers the change needs in parallel (fresh context), skips the rest on the record (scripts/skip-pre-push-review.sh), loops to ship_it. all forces the full set |
.claude/agents/pre-push-reviewer.md | pre-push-reviewer subagent — canonical pre-push code review (one of the parallel pre-push reviewers in scripts/pre-push-reviewers.sh); also used for auditing others’ PRs locally |
.claude/agents/docs-reviewer.md | docs-reviewer subagent — docs-prose + code↔docs-sync review; mandatory pre-push gate (writes tmp/docs-reviewer-passed-<sha> for the commit on its REVIEWED: line on ship_it, in parallel with the other pre-push reviewers); advisory only for ad-hoc path/all. Scope via scripts/docs-prose.sh |
.claude/skills/pr-sync-with-main/SKILL.md | “Fix this stale PR” workflow — merge origin/main, never rebase or force-push |
.claude/skills/pr-review-locally/SKILL.md | “Review PR wt switch pr:<N> + the relevant reviewers (code, docs, …) in parallel, no PR comments |
.claude/skills/pm-triage/SKILL.md | PM-review workflow — triage feedback / backlog / code TODOs and reconcile issue & PR status into well-scoped, tracked Task Board (project #7) issues; invoked as /pm-triage |
.claude/settings.local.json | Your personal overrides — gitignored; put model choice, status line, allow lists, etc. here |
.config/wt.toml | Worktrunk project hooks (post-start, pre-merge, pre-remove) |
Notably absent: no .mcp.json, no committed status line, no permissions.allow list. See GitHub® access and Permission posture below.
Slash commands
Section titled “Slash commands”| Command | What it does |
|---|---|
/cover [suite] | Renders coverage for a suite (unit / integration / e2e / sdk / all / merge) and surfaces drops below threshold |
/docs-review [path|all] | Runs the docs-reviewer subagent — accuracy vs code, runnable examples, clarity, completeness, + code↔docs sync. No-arg gates the push (writes the docs marker on ship_it); a path/all is advisory. Complements misspell / markdownlint / links-validator |
/prepush [all] | The mandatory pre-push self-review — judges which reviewers in scripts/pre-push-reviewers.sh the change needs, runs those in parallel (fresh context), skips the rest on the record (logged), and loops until each reaches ship_it. all forces the full set |
To add a command: drop a .md file in .claude/commands/. Filename becomes the slash command. Frontmatter: description and argument-hint; body is the prompt with $ARGUMENTS.
Subagents
Section titled “Subagents”| Subagent | When to use |
|---|---|
pre-push-reviewer | Mandatory before pushing to a PR branch (enforced by .claude/hooks/agent-bash-gate.sh), run in parallel with the other reviewers in scripts/pre-push-reviewers.sh — all must reach ship_it. Also used for auditing someone else’s PR after wt switch pr:<N>. Runs the canonical .github/prompts/pr-review.md workflow against the local branch in fresh context. Fetches PR comments + CI status + linked-issue acceptance criteria when on a PR branch. Returns [MUST]/[SHOULD]/[MAY] findings + parseable REVIEWED: <sha> and VERDICT: ship_it|iterate|block lines that drive the tmp/pre-push-reviewer-passed-<sha> marker. |
docs-reviewer | Mandatory before pushing to a PR branch — runs in parallel with the other pre-push reviewers (all enforced by .claude/hooks/agent-bash-gate.sh). Reviews docs prose (accuracy-vs-code, runnable examples, clarity, completeness) and code↔docs sync (code that changed but whose docs didn’t), using .github/prompts/docs-review.md over the scripts/docs-prose.sh denylist set (Starlight site + governance docs incl. the SDK readme). Default (branch) scope emits REVIEWED: <sha> and VERDICT: ship_it|iterate|block → writes tmp/docs-reviewer-passed-<sha>; a path/all is advisory (no marker). Posts no PR comments, never edits docs. Complements misspell / markdownlint / starlight-links-validator, never duplicates them. |
Invoke via the Agent tool with subagent_type: pre-push-reviewer, or via /agents.
To add a subagent: drop a .md file in .claude/agents/. Frontmatter: description (used by main Claude to decide when to delegate), tools, model. Body is the system prompt. To make a reviewer a mandatory pre-push gate (the way pre-push-reviewer and docs-reviewer are), also add its name to scripts/pre-push-reviewers.sh — the single source of truth the hooks and /prepush read, so no hook needs editing. The subagent itself must pin the commit with git -C <worktree> rev-parse HEAD before reading anything, where <worktree> is the worktree its prompt names, else its current directory, and end its report with the parseable REVIEWED: <that sha> and VERDICT: ship_it|iterate|block lines; without both, its ship_it writes no marker (tmp/review-marker.log says why). See AGENTS.md §“Adding a pre-push reviewer”.
Skills
Section titled “Skills”Skills load automatically into Claude’s context when conversation patterns match their description.
| Skill | Triggers on |
|---|---|
pr-sync-with-main | When a PR shows “out-of-date with base branch”, or a user asks to “fix the PR” / “sync with main”. Documents the merge-not-rebase procedure and the WaveHouse-specific reason long-lived branches need it. |
pr-review-locally | When a user asks to “review PR wt switch pr:<N> (or gh pr checkout), runs the relevant reviewers (code, docs, …) in parallel in fresh context, surfaces their combined findings without commenting on the PR. |
pm-triage | When a user asks to triage dogfooding feedback, re-prioritize the backlog (P0–P3), sweep code TODOs, or check that work is tracked — runs a PM-style review against the Task Board (project #7) and proposes well-scoped issues. Invoked as /pm-triage. |
To add a skill: create .claude/skills/<name>/SKILL.md with frontmatter name + description and the workflow body. Description quality matters — that’s what Claude matches against to load the skill.
Agent PR Discipline
Section titled “Agent PR Discipline”Agents (Claude Code etc.) have additional gating beyond what humans face — enforced by .claude/hooks/agent-bash-gate.sh (PreToolUse Bash) + deny rules in .claude/settings.json. Humans keep full git/gh affordances; agents have these extra constraints:
- Drafts only.
gh pr create(or its aliasgh pr new) must include--draft. Only humans transition draft → ready (gh pr readyis blocked), approve (gh pr review --approveis blocked), or request changes (gh pr review --request-changesis blocked). The gate also lints the PR title ongh pr create/gh pr new/gh pr editwith a literal title viascripts/lint-pr-title.sh— the same rule as the requiredCIcheck’sPR titlejob, so a malformed title is caught before the PR exists (fail-open when the title is missing or computed). - The API forms count too. Through
gh api, opening a PR (…/pulls) needs-F draft=true; a review withevent=APPROVEorevent=REQUEST_CHANGES, GraphQLcreatePullRequestwithoutdraft: true,markPullRequestReadyForReview,requestReviewsand a merge (…/pulls/<N>/merge,mergePullRequest,enablePullRequestAutoMerge) are blocked.gh pr mergeitself is left to thedenyrule in.claude/settings.json. The gate searches the command’s text rather than parsing it, so only plain forms are caught and a command that merely mentions one of these names is blocked too (a false positive); put long text in a file (--body-file,git commit -F <file>,gh api -F body=@file). A form the gate doesn’t catch is still against the rules. - No human reviewer assignment.
gh pr edit --add-reviewer / --add-assignee,--reviewer/--assigneeongh pr create/gh pr new, and anygh apiwrite to…/requested_reviewersare blocked. GitHub assigns reviewers natively — therequired_reviewersruleset rule requests the@Wave-RF/wavehouse-adminsteam and the team’s code-review assignment picks the member; humans handle the rest. - Bot re-triggers via comments. Agents CAN mention bots in PR comments to re-trigger reviews —
@coderabbitai review, etc. This goes throughgh pr comment(allowed), not the reviewer API. - Pre-push review required on PR branches. Before
git pushto a branch with commits ahead ofmain, the agent runs the reviewers inscripts/pre-push-reviewers.sh(today:pre-push-reviewerfor code,docs-reviewerfor docs prose + code↔docs sync) that the change needs — in fresh context, in parallel — and skips any with nothing to do viascripts/skip-pre-push-review.sh <name> "<reason>"(a logged skip that satisfies the marker, so a docs typo doesn’t pay for a full code review)./prepushdoes all of this.ship_itrequires zero findings at any severity — any[MUST]/[SHOULD]/[MAY]forces iterate; fix and re-invoke (always fresh context) until clean. The push gate requires a marker — from aship_itor a logged skip — from every listed reviewer for the commit each refspec publishes (its tip, or HEAD with no refspec), judged in the worktree the push runs in, and echoes the skips at push time. Aship_itmarks only the commit the reviewer read, so don’t commit in a worktree while its reviewers run. - Don’t bypass.
--no-verifyand hand-writing markers are policy violations, not regex-blocked. An agent that wants to bypass can edit the gate itself — trust the policy in AGENTS.md §“Agent PR Discipline”. Markers come frommake ci, thereview-marker.shhook, andscripts/skip-pre-push-review.sh(logged skips); nothing else. - PR reviews on others’ PRs stay local. Use the
pr-review-locallyskill for local-only audits — the reviewers’ findings go to you, not the PR.
Full ruleset and rationale: AGENTS.md §“Agent PR Discipline”.
When a hook script is missing
Section titled “When a hook script is missing”Each hook in .claude/settings.json names its script under $CLAUDE_PROJECT_DIR, which the Claude Code docs define as “the project root where the session started”, and which stays put when the session enters a worktree. If that checkout is removed while the session runs, or a script goes missing or loses its executable bit, a bare path would make the shell exit 127, and Claude Code treats any exit code but 2 as a non-blocking error: the push gate would stop gating without a word. So each command is a short guard that execs the script when it’s there and executable, unchanged, and otherwise acts by event:
| Event | Script | With the script missing |
|---|---|---|
| PreToolUse (Bash) | agent-bash-gate.sh | Blocks (exit 2) a command that contains the words git and push; gh and pr with one of create, new, edit, ready, review or merge; or gh and api with pulls or graphql, which covers every call to the pull-request or GraphQL API, reads included. It also blocks every command when there is no jq to read it with. Other commands go through, so the session can still work |
| SubagentStop | review-marker.sh | Reports it on stderr with exit 1. Exit 2 would keep the subagent running. A reviewer’s ship_it writes no marker, so the push gate blocks for lack of one |
| PostToolUse | gofumpt-on-save.sh, markdown-on-save.sh | Reports it with exit 2, which shows Claude the message; the edit has already happened |
Every message names the missing script and says to restart Claude Code from a live checkout. The PreToolUse match is crude on purpose: like the gate, it guards against accidents, not deliberate workarounds, and it errs toward blocking.
Worktrunk integration
Section titled “Worktrunk integration”We use worktrunk as the recommended worktree manager. It creates real git worktree add directories, so the committed .claude/ config Just Works in every worktree.
Project hooks in .config/wt.toml:
| Hook | Command | Why |
|---|---|---|
post-start | wt step copy-ignored, then make tools (an ordered [[post-start]] pipeline) | Seeds the gitignored caches listed in .worktreeinclude (node_modules/, .bin/, build outputs) from the primary checkout, then bootstraps tools, modules, pnpm deps, and git hooks. The install runs after the copy, so it decides the final node_modules layout, not the copy |
pre-merge | make verify | Fast pre-merge gate (same as pre-commit) |
pre-remove | git status --short | Surfaces uncommitted work before tearing down |
Common workflow:
wt switch -c feat/new-thing # fresh worktree off origin/mainwt switch -x claude -c feat/x # spawn an agent in a new worktreewt list # status of all worktreeswt merge # squash + rebase + merge + clean upwt remove feat/new-thing # tear down (warns on uncommitted work)User-specific worktrunk config goes in ~/.config/worktrunk/config.toml; the committed .config/wt.toml has team-wide hooks only.
GitHub access: gh CLI vs MCP
Section titled “GitHub access: gh CLI vs MCP”We use gh CLI as the canonical GitHub access path, not a GitHub MCP server. Reasons:
ghis already a hard dev requirement- Works identically in Claude Code, terminal, and shell scripts
- No extra auth / approval / npx cold-start
The GitHub MCP server is useful for cross-repo code search and bulk graph queries, but neither is a daily WaveHouse pattern. If you want it, add at user level:
// ~/.claude.json — your user-level config{ "mcpServers": { "github": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}" } } }}Then export GITHUB_TOKEN=$(gh auth token) in your shell rc.
Other useful MCP servers (user-level, optional)
Section titled “Other useful MCP servers (user-level, optional)”None of these are committed at project level — pick what you actually use.
- Grafana® MCP — for querying Prometheus® / Loki™ / Tempo™ / Pyroscope™ from Claude when debugging observability work. Useful if you touch
internal/observability/. - ClickHouse® MCP (community) — direct schema introspection + query against your local
make devClickHouse. Useful forinternal/discovery/and ingest work, butmake deps-shell(clickhouse-client REPL) is often enough.
make obs-grafana brings up a local Grafana LGTM stack (see Deployment → Local Observability Stack), so Grafana MCP pointed at it is already the natural choice for trace / log inspection — no heavyweight cluster to stand up first.
Permission posture
Section titled “Permission posture”.claude/settings.json is structured around the reality that most contributors run Claude Code in bypassPermissions (yolo). In that mode:
- ✅
permissions.denystill fires — the only programmatic guardrail - ❌
permissions.allow/permissions.askare moot (everything’s already allowed) - ✅ Hooks (Claude Code AND git) still fire — the behavioral layer
So the file is deny-only by design. Personal allow / ask lists go in .claude/settings.local.json (gitignored).
defaultMode is intentionally not set in committed config. Each user picks their preferred mode.
The deny list blocks:
| Blocked | Why |
|---|---|
git push --force, -f, --force-with-lease | Force-pushing is destructive; also loses inline review-comment anchors |
git reset --hard origin:* | Throws away local work |
git filter-branch, git update-ref -d | History-rewriting / ref destruction |
gh pr merge, gh repo delete, gh release delete | Irreversible / shared-state |
gh pr ready, gh pr review --approve / -a / --request-changes / -r | Agent PR Discipline — draft→ready and review verdicts are human actions |
gh workflow disable, gh secret delete | Operational footguns |
rm -rf /, rm -rf ~, rm -rf $HOME, sudo rm | Filesystem destruction |
Status line, output style, model
Section titled “Status line, output style, model”Not committed at project level. Personal preference — put in .claude/settings.local.json:
{ "statusLine": { "type": "command", "command": "~/.config/claude/statusline.sh" }, "outputStyle": "default", "model": "claude-opus-4-7"}Daily workflow
Section titled “Daily workflow”- Write code (gofumpt-on-save formats Go files as you go; markdown-on-save does the same for
.md/.mdx). git commit→ pre-commit hook runsmake verify(or skips ifmake cialready validated this tree). Fix anything that fails.git push→ the pre-push hook blocks until the tree is validated for what changed —make cifor a code change,make verifyfor a docs/prose-only one (run it, fix, retry). For agents, the agent-bash-gate hook also requires a marker for the pushed commit from every reviewer inscripts/pre-push-reviewers.shwhenevermaindoesn’t have that commit yet — including the first push, before the PR exists. Run/prepush→ it judges which reviewers the change needs, runs those in parallel (on Ship it each marker auto-writes), and skips the rest on the record (skip-pre-push-review.shwrites their markers + logs why) → push succeeds. On Iterate/Block, fix, re-invoke (fresh context each time), repeat.- Open the PR with
gh pr create --draft(agents required to use--draft; humans flip to ready when ready). - CI workflows fire on the new HEAD. Address review comments per AGENTS.md §Review Response.
Helpers along the way:
- Stale PR (“out-of-date with base branch”) → ask Claude to “fix the PR” (loads
pr-sync-with-mainskill — merges main, doesn’t rebase or force-push). - Reviewing someone else’s PR → “review PR 120 locally” / “audit PR 120” (loads
pr-review-locallyskill —wt switch pr:120+ the relevant reviewers in parallel, no PR comments). - Re-trigger a bot reviewer → ask Claude to “ping coderabbit again” — Claude posts the appropriate
@<bot>comment on the PR. - Coverage check on a specific suite →
/cover unit(orintegration,e2e,sdk,all).
Memory: Claude Code maintains per-project memory in ~/.claude/projects/<slug>/memory/. AGENTS.md is the SHARED source of truth; memory is for personal observations / preferences that don’t belong in committed config.
Extending
Section titled “Extending”The .claude/ layout is intentionally small. Add things when they earn their keep:
- New slash commands as recurring workflows emerge
- New subagents for specialized one-shot tasks
- New skills for context-loaded workflow patterns
- New Claude Code hooks for edit-time UX gaps (NOT for enforcement — gates go in
.githooks/so they apply to humans too) - Adjust
.githooks/pre-commit/pre-pushif the gates feel wrong
If you build something useful, commit it and update this doc.
Reference
Section titled “Reference”- AGENTS.md — project conventions, architecture, code style, doc-sync rules, SDK-sync rules, branch maintenance. Source of truth for all AI agent work.
- Claude Code docs — official Claude Code reference.
- worktrunk.dev — worktree manager.
.github/prompts/pr-review.md— the canonical review prompt thatpre-push-reviewerruns locally.