Hooks
Hooks (opens in a new tab) are scripts Claude Code runs at set moments: before or after a tool call, when a session starts or stops, and when you send a message. GAIA registers them in .claude/settings.json. The app’s frontend/.claude/settings.json is generated from the root settings plus an overlay file, so the same hooks run in both places. The scripts live in .claude/hooks/.
Unlike rules, which Claude reads and follows, hooks are scripts that always behave the same way. A guard hook turns back one of Claude’s tool calls no matter what the model decided. Its message goes to Claude, usually with the allowed form of the command or the next step, and Claude retries that way, with nothing for you to do. Guards act on Claude’s tool calls only: a command you run yourself in a terminal never meets them. A permission mode that skips prompts, such as auto mode, does not skip hooks.
Claude never uses cd in a command (the shell-cwd rule), because a cd into another folder can switch GAIA’s guards off without warning. When jq is missing, a guard hook turns the call back rather than letting it through unchecked; installing jq is yours (see jq not installed).
Why each guard exists, and how to clear it, is in the gate reference.
Guards (can turn back a tool call)
Section titled “Guards (can turn back a tool call)”Bash and Monitor commands
Section titled “Bash and Monitor commands”| Hook | Turns back Claude’s |
|---|---|
block-main-destructive-git | Commits on main or master, force-pushes that name them, any push from them or that pushes main, master, or HEAD, a git reset --hard to any other commit on any branch, and a git checkout or git switch that would move the main checkout off another session’s open-PR branch. |
block-no-verify | git commit or git push with --no-verify, -n on a commit, or -c core.hooksPath=<path>. |
block-rm-rf | rm -rf of /, an absolute path outside the allowed scratch folders, $HOME, ., *, dotfile globs, .git, .claude and its children, and node_modules, plus --no-preserve-root. Scratch paths and the session scratchpad are allowed. |
block-sensitive-read | Reading dotenv files (.env and every .env.<name> except .env.example), keys (.key, .pem), files whose name contains credential (except TypeScript source), and anything under a secrets directory, through any Bash command that reads files, source, redirects, and filter flags such as rg -g '*.key'. Also plain env or printenv, which print every environment variable. Also registered on Read and Grep. |
block-manifest-write | Writes to .gaia/manifest.json, which lists only the files GAIA ships. /update-gaia writes it through a specific exemption. |
block-invalid-pr-title | A gh pr create or gh pr edit whose title would fail the PR Conventions lint once it becomes the squash-merge commit message (the title plus (#<n>)), and --fill with no --title. |
block-machine-local-ref-gh-text | A gh pr create, gh pr edit, gh issue create, or gh issue edit whose title or body names a SPEC, UAT, or plan number, or a path to a specific entry under .notebook/ or .gaia/local/. See the gate. |
block-pr-create-without-draft | A gh pr create for this repository without --draft. |
block-fork-pr-checkout | gh pr checkout or a git fetch of pull/<n>/head or pull/<n>/merge for a pull request from a fork, or one that gh cannot identify as fork or not. |
block-handrolled-pr-poll | A shell loop that checks gh pr view or gh pr checks over and over without reading mergeable. Claude uses GAIA’s merge wait instead. |
pr-merge-audit-check | gh pr merge until every Code Audit Team member responsible for the changed files has approved it. For a pull request outside the audit’s scope, or a chore(deps) pull request that only changes dependency manifests, it posts the skipped: status itself. |
worthiness-presence-check | gh pr merge when a changed test has no worthiness record for its current content. In scope: stories, Playwright (opens in a new tab) tests, and component or page tests whose result the classifier finds is not predictable. It checks only that the record exists, not its verdict. |
red-verify-commit-check | git commit of a new, passing test for code with predictable output (utilities, parsers, hooks) that was never seen failing. Exemptions are listed under the RED gate. |
block-audit-loop-write | Writes to the audit loop’s protected state and context readings, running by hand the scripts that record your grant, and running the local-state cleanup scripts that only a hook may start. |
Edit, Write, and MultiEdit
Section titled “Edit, Write, and MultiEdit”| Hook | Turns back Claude’s, or asks you about |
|---|---|
block-secrets-write | Content that contains an AWS key, a GitHub token, a private key header, or a variable with a secret-sounding name set to a real-looking value (including export, declare, and local forms), and any write to a .env file except .env.example. |
block-eslint-config-edit | Asks you to confirm any edit to an eslint.config.* file, and explains why: a config edit can silence a rule just as easily as it can make an approved migration. |
block-manifest-write | Writes to .gaia/manifest.json. |
block-worktree-path-mismatch | An edit whose path points into a different checkout than the worktree the session is working in. Writes to the main checkout’s .notebook/ are exempt. |
block-audit-loop-write | Writes to the audit loop’s protected state. |
Agent and Task dispatches
Section titled “Agent and Task dispatches”| Hook | Limits |
|---|---|
audit-loop-bound | Each start of the audit loop or of a Code Audit Team member, per branch. It checks the context checkpoint, whether rounds are still making progress, and the hard limit on rounds, and holds the loop at a checkpoint until you answer. |
Recorders and context
Section titled “Recorders and context”These hooks never turn anything back. They record what happened (test failures, grants, usage) or add guidance to the session at the right moment.
| Hook | Event | What it does |
|---|---|---|
audit-loop-grant | UserPromptSubmit | Records audit-grant <n> or audit-accept, typed as your whole message in an interactive session. |
audit-loop-ask-grant | PostToolUse (AskUserQuestion) | Records your answer to the audit checkpoint’s question. |
post-findings-block-on-merge | PreToolUse (gh pr merge) | Posts one comment collecting the pull request’s audit findings, which /gaia-codify later counts. Never blocks. |
capture-red-observations | PostToolUse and PostToolUseFailure (Bash) | Records failing test runs, so the RED gate can see a test failed before it passed. |
capture-gh-artifact | PostToolUse (gh pr create) | Links the pull request to its branch in the usage ledger. |
pr-merge-cost | PostToolUse (gh pr merge) | Prints the pull request’s cost box. |
issue-claim-release | PostToolUse (gh pr merge) | Removes in-progress from every issue the merged pull request closes, once the merge is confirmed. |
debt-sentinel-touch | PostToolUse (gh issue and merge commands) | Refreshes the statusline’s tech-debt count. |
workflow-doctrine-inject | SessionStart, PostToolUse (EnterWorktree, git, gh pr checkout) | Adds GAIA’s working rules to the session when it works on a branch or in a worktree (the roles are described in Who does what), and redirects a plain git worktree add under .claude/worktrees/ to EnterWorktree. |
context-checkpoint-inject | PostToolUse | When context use reaches the red, fire, or skull level on the statusline, tells the main session to save its place and ask whether to continue or clear. |
context-checkpoint-subagent | PostToolUse | The same checkpoint for a sub-agent that was given a file to save its place in. |
provision-worktree | SessionStart, PostToolUse (EnterWorktree) | Links shared files into a worktree, gives it its own set of ports and a port file, and installs dependencies and typed routes. After /clear or compaction it repairs only a worktree that was never set up, so a set-up worktree costs nothing. |
ports-session-start | SessionStart | Records which session started which dev server, and stops servers left behind by ended sessions and removed worktrees. |
wiki-session-start | SessionStart | Cleans up after a /gaia-wiki run whose session ended before its merge finished (deletes the leftover local wiki branch and updates the base branch), and lists merged worktrees with the commands to remove them. |
debt-session-reconcile | SessionStart | Re-checks the /gaia-debt count shown on the statusline against GitHub. |
usage-capture | SessionStart, Stop | Records the session’s model usage to the local usage ledger. |
state-reaper-session-start | SessionStart | At most once every 24 hours, starts a background cleanup of spent entries in .gaia/local (state left by merged branches and removed worktrees, and entries past their age limit). The usage ledger and your price override are never removed. |
Not hooks, but enforced
Section titled “Not hooks, but enforced”- Native git hooks in
.githooks/:pre-commit(typecheck, lint-staged (opens in a new tab), tests) andcommit-msg(commitlint (opens in a new tab)). Both also catch machine-local references. They run on every commit, Claude’s or yours: Claude fixes what they report and commits again, and when you commit by hand you can ask Claude to fix it. See Git hooks and conventions. - Permission rules in
.claude/settings.json: for example, Claude cannot editpnpm-lock.yamldirectly;pnpm installregenerates it. - The
GAIA-Auditrequired check on GitHub, registered by/setup-gaia. Claude posts it on every merge it runs, so it meets you only when you merge with the button on github.com.
Adding your own
Section titled “Adding your own”Ask Claude to write and register the hook for you. When Claude works in .claude/hooks/, the file-placement rule loads and tells it where the script goes, how to register it, and how to check that the hook finds its script from every folder. For example:
Add a Claude Code hook that <what it should do, and when>. Register it so it works from every folder, and regenerate the frontend settings.Claude tests a new hook with a sample call it should refuse and one it should allow, so you know it can fail.
To turn off or replace one of GAIA’s hooks, ask Claude. It first names what the hook protects and confirms which call you want it to stop refusing. It then removes the hook’s entry from .claude/settings.json and leaves the script in place, since an update would only restore a deleted script. A replacement goes in as a hook of your own, never as an edit to GAIA’s script. Claude regenerates the frontend settings and checks that the call now passes:
Turn off GAIA's <hook name> hook. Tell me what it protects first.The file-placement rule’s hook-rooting check (it confirms each registered hook can find its script whatever folder the session is in) covers hooks registered in .claude/settings.json only. A hook you add to .claude/settings.local.json is not checked.