# Make Claude Code run tests before it pushes

To make Claude Code run your tests before every push, block the push with a hook. Instructions in `CLAUDE.md` help, but Anthropic's docs say Claude treats them as [context, not enforced configuration](https://code.claude.com/docs/en/memory). A hook runs whether or not Claude remembers.

| Method | What enforces it | When it runs | Other agents |
|---|---|---|---|
| `CLAUDE.md` or `AGENTS.md` | Nothing: Claude reads it | When Claude follows it | `AGENTS.md` only |
| [`PreToolUse` hook](#pretooluse) | Claude Code | Before a matching command | No |
| [`Stop` hook](#stop-hook) | Claude Code | When Claude finishes | No |
| [Git `pre-push` hook](#git-hook) | Git | On every `git push` | Yes, and people too |

## Say it in CLAUDE.md

Start here, since a blocked push wastes a turn:

```markdown title="CLAUDE.md"
## Before you push

Run `npm test` and fix what fails before `git push`.
```

## Block the push with a hook

A [`PreToolUse` hook](https://code.claude.com/docs/en/hooks) runs before a tool call, and exit code 2 blocks the call. Commit this to the repository, so everyone who works in it gets it:

```json title=".claude/settings.json"
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(git push *)",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/test-before-push.sh",
            "timeout": 1800,
            "onFailure": "block"
          }
        ]
      }
    ]
  }
}
```

```bash title=".claude/hooks/test-before-push.sh"
#!/bin/sh
# Without jq the check below can't read the command, so refuse.
command -v jq >/dev/null || { echo "This hook needs jq." >&2; exit 2; }

# The hook's `if` filter is best-effort, so check the command here too.
case "$(jq -r .tool_input.command)" in
  *"git push"*) ;;
  *) exit 0 ;;
esac

# Exit code 2 blocks the push, and Claude reads what's on stderr.
if ! npm test >&2; then
  echo "Tests failed: fix them, then push again." >&2
  exit 2
fi
```

```bash
chmod +x .claude/hooks/test-before-push.sh
```

This setup was tested on Claude Code 2.1.316 on November 1, 2026. With a failing test, it blocked `git push`, `git push origin main`, `npm test && git push` and `FOO=bar git push`, and let other commands through. With a passing test, the push went ahead. With `jq` missing, it refused.

Three details decide whether it holds:

- **Check the command in the script.** The `if` filter is best-effort. A pattern such as `Bash(git push *)` also runs the hook on commands that use `$()` or `$VAR`. In the test, it ran on `echo $(date)`, which would have blocked that command while the tests were failing, and it didn't run on `ls`.
- **Set `onFailure` to `block`.** Without it, a hook that crashes, times out or has a wrong path lets every push through. It needs Claude Code 2.1.295 or later.
- **Set `timeout` above your suite's time.** The default is 600 seconds. With `onFailure` set to `block`, a timeout blocks the push, so a suite that takes longer can never push.

Anthropic's docs point to the permission system for a hard deny, because `if` is a filter and not a boundary. That's a reason to [add a Git hook](#git-hook) as well.

## Send Claude back if tests fail

A `Stop` hook runs when Claude finishes, and exit code 2 sends it back to work:

```json title=".claude/settings.json"
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/tests-before-stop.sh",
            "timeout": 1800
          }
        ]
      }
    ]
  }
}
```

```bash title=".claude/hooks/tests-before-stop.sh"
#!/bin/sh
# stop_hook_active is true when Claude is already continuing because of this hook.
if [ "$(jq -r .stop_hook_active)" = "true" ]; then
  exit 0
fi

if ! npm test >&2; then
  echo "Tests are failing. Fix them before you finish." >&2
  exit 2
fi
```

```bash
chmod +x .claude/hooks/tests-before-stop.sh
```

Weigh three things before you use one:

- **It sends Claude back once.** The `stop_hook_active` check lets the second stop through, whether or not the tests pass. Without that check, the loop goes on until they do, or until Claude Code ends it after eight continuations in a row with no tool call between them.
- **It runs on every stop,** even when Claude only answered a question.
- **It can reward the wrong fix.** In the same test, with a check that could only fail, Claude Code on its Haiku model rewrote the test so that it always passed, and said so in its report. Read changes to test files in review.

## Add a Git hook as the backstop

Claude Code's hooks bind only Claude Code. A `pre-push` hook is Git's own, so it also covers Codex, Cursor and people:

```bash title=".git/hooks/pre-push"
#!/bin/sh
npm test
```

```bash
chmod +x .git/hooks/pre-push
```

Git doesn't clone hooks, and `git push --no-verify` skips them. [Run CI locally before you push](https://hushrun.com/docs/guides/run-ci-locally.md#hook) covers both.

## Make the hook cheap with hushrun

Every method above runs the whole suite, and then CI runs it again. [`hushrun`](https://hushrun.com/docs/for-agents.md) changes both:

- **`npx hushrun init`** writes a skill Claude Code reads, a block in `AGENTS.md` and a `pre-push` hook.
- **The hook runs no checks.** It asks what CI would still run on this exact code, so a long suite can't time out a push. It refuses a push from Claude Code, Codex, Cursor or OpenCode until those checks have passed, and warns people instead of refusing.
- **A pass counts everywhere.** `hushrun run` records it for these exact files, and CI skips the check. A `Stop` hook that runs a wrapped check costs nothing on code that already passed.

```console
$ git push
hushrun: push refused: CI would run test again on a0b1c2d
  test: no pass yet for this exact code
Run these here, then push again:
  npm run test
```

Claude reads those lines, runs the command and pushes again. `--no-verify` still gets past the hook, and nothing is lost when it does: CI runs whatever has no pass.

## Related

- [For coding agents](https://hushrun.com/docs/for-agents.md)
- [Keep CI useful with coding agents](https://hushrun.com/docs/guides/coding-agents-ci.md)
- [Run CI locally before you push](https://hushrun.com/docs/guides/run-ci-locally.md)
