goalline / Claude Code hook
Stop Claude Code from editing tests to pass
goalline guard is a Claude Code Stop hook. Before the agent may finish, the old versions of the tests its work edited run against that work. If an old expectation no longer holds, the agent keeps working until it restores the behaviour or records why the change is intended.
What the agent sees
When Claude Code is about to stop, the hook runs goalpost on the work as it stands, uncommitted files included, against the base branch. If an old test expectation no longer holds, the hook exits 2 and its message goes back to the agent: which tests, with the exact old and new values, and what to do about each one.
For each one the agent either restores the behaviour or records why the change is intended:
goalline guard --ack "src/cart.spec.ts::Cart applies the discount" --why "the task asks for the discount to apply before tax"
Editing the test to match does not get past the guard, because it runs the old version of the test. The reasons form the reviewer's ledger: goalline guard --report prints them as Markdown for the pull request.
The reasons go in .goalpost-acks.json. Commit it with the change, and the GitHub Action shows each one on its card, next to the old and new values. Only the entries the change adds count: one already on the base branch explained an earlier change.
Set it up
Install goalline once (npm install -g github:intikhab49/goalline), then add the hook to .claude/settings.json. The timeout is in seconds; a cold Jest run can take more than a minute.
{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "goalline guard --hook --base origin/main -- npx jest {files}", "timeout": 600 }
]
}
]
}
}
For a monorepo, add --cwd apps/web. For Vitest or pytest, change the command after --.
It never traps the agent
- The guard never blocks because of its own failure: if the check can't run, the agent may stop.
- After
--max-blocksidentical blocks (default 3), it lets the agent stop with a warning that a person must review the changes. - Nothing in the checkout changes: the snapshot of the work is a loose git object, not a commit on the branch.
Other agents
The guard is a plain command: run goalline guard -- <test command> as the last step of any agent loop. Exit 2 means an old expectation moved and the work isn't done; exit 0 means it may stop.
Related
- How it works: old tests on new code, the verdicts, and what it can't catch
- GitHub Action: one step in a workflow, with Jest, Vitest or pytest
- prove-red: new tests must fail on the base code first, or they test nothing
- Skip what passed: signed receipts keyed by the git tree, and safe reuse
- Database inputs: the database schema on the receipt, with --input db=pg
- The clock: which tests read the real clock, and passes that expire
- Benchmark: 31 flags on 400 merged PRs, each with its PR's reason
- Commands: every command and option