goalline How it worksClaude Code hookGitHub ActionBenchmarkCommandsGitHub

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

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