PricingSign inDownload

Blog / git

Stop your coding agent from committing secrets

A git pre-commit hook with gitleaks blocks any commit that adds a key, from Claude Code, Codex or you. Plus the rule that stops an agent from skipping it.

On this page
  1. The pre-commit hook
  2. What we saw when we tested it
  3. Stop the agent from skipping the hook
  4. If a key already got committed
  5. Where Joxo fits
  6. Frequently asked questions
  7. Does a pre-commit hook work with Claude Code and Codex?
  8. Can a coding agent skip a git pre-commit hook?
  9. Will gitleaks slow down commits?
  10. What if gitleaks flags a fake key in a test?
  11. Try it with your team

An agent writes a config file, puts a real key in it so the code runs, and then commits everything with git add .. Most of the time the agent notices and asks first. Sometimes it doesn't, and a key that only lived on your laptop is now in the history, waiting for the next push.

Telling the agent "never commit secrets" in CLAUDE.md or AGENTS.md helps, but it's still a request. A git pre-commit hook turns it into a rule. Git runs it before every commit, whoever makes the commit: you, Claude Code, Codex, Cursor or any other agent. If the hook fails, the commit doesn't happen.

Below is the hook we use, what happened when we tested it with Claude Code, and the second rule you need so the agent can't skip the hook.

The pre-commit hook#

gitleaks scans text for keys and tokens: cloud provider keys, Stripe and GitHub tokens, private keys, and generic high-entropy strings next to words like api_key. Install it first:

sh
brew install gitleaks        # macOS
# Linux and Windows: download a release from github.com/gitleaks/gitleaks/releases
gitleaks version

Keep the hook inside the repository so every clone and every worktree gets it. Create .githooks/pre-commit:

sh
#!/bin/sh
# Block commits that contain secrets.
if ! command -v gitleaks >/dev/null 2>&1; then
  echo "pre-commit: gitleaks is not installed; commit blocked." >&2
  exit 1
fi
exec gitleaks git --staged --redact --no-banner --verbose

Then make it executable and point git at the folder:

sh
chmod +x .githooks/pre-commit
git config core.hooksPath .githooks

--staged scans only what is about to be committed, so it takes milliseconds. --redact keeps the secret itself out of the output, which matters because the agent reads that output into its context. The command -v check makes a missing gitleaks fail loudly instead of letting every commit through.

core.hooksPath is a per-clone setting, so each teammate (and each fresh clone an agent makes) needs to run that git config line once. A line in your README or setup script covers it.

What we saw when we tested it#

We put a Stripe live key in config.env, staged it, and ran the hook the way git does before a commit. Here is the output, with the timestamps removed:

Finding:     STRIPE_KEY=REDACTED
Secret:      REDACTED
RuleID:      stripe-access-token
Entropy:     4.025071
File:        config.env
Line:        1
Fingerprint: config.env:stripe-access-token:1
0 commits scanned.
scanned ~281 bytes (281 bytes) in 24.9ms
leaks found: 1

The hook exited with code 1 and git refused the commit. The agent sees the file and the rule name, which is enough to move the value into an environment variable or an ignored file.

We also asked Claude Code (Haiku) to commit the same files. In that run it spotted the key on its own and stopped to ask, which is the good case. The hook is there for the runs where it doesn't spot it: a key in a file it didn't read, a long session, or a different agent.

Stop the agent from skipping the hook#

Git has a flag that skips the pre-commit hook: --no-verify, or -n for short. An agent that hits a failing hook and wants to finish its task may reach for it. Claude Code lets you block that in two ways.

A deny rule. In .claude/settings.json:

json
{
  "permissions": {
    "deny": [
      "Bash(git commit --no-verify:*)",
      "Bash(git commit -n:*)"
    ]
  }
}

We asked Claude Code to run git commit --no-verify -m 'skip hooks' with this file in place. It got "Permission to use Bash with command git commit --no-verify -m 'skip hooks' has been denied", and no commit was made. Deny rules match the start of the command, though, so git commit -m 'x' --no-verify (flag at the end) gets past them.

A PreToolUse hook catches the flag anywhere in the command. Save it as .claude/hooks/keep-git-hooks.sh and make it executable:

sh
#!/bin/sh
# Refuse any git commit or push that skips git hooks.
cmd=$(jq -r '.tool_input.command // empty')
case "$cmd" in
  *git*commit*|*git*push*) ;;
  *) exit 0 ;;
esac
if printf '%s' "$cmd" | grep -Eq -- '--no-verify|(^|[[:space:]])-[a-zA-Z]*n[a-zA-Z]*([[:space:]]|$)|core\.hooksPath'; then
  echo "Blocked: this command skips git hooks. Fix what the hook reported instead." >&2
  exit 2
fi
exit 0

And register it in .claude/settings.json:

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/keep-git-hooks.sh" }
        ]
      }
    ]
  }
}

We fed it eight commands. It blocked git commit -m x --no-verify, git commit -nm x, git -c core.hooksPath=/dev/null commit -m x and git push --no-verify origin main, and let git commit -m 'add notes', git commit -am fix and git push origin main through. It also blocked git commit -m "run -n mode", because a message containing -n looks like the flag. That false alarm is rare and easy to reword.

In a real session, Claude Code got: "Blocked: this command skips git hooks. Fix what the hook reported instead." It did not retry another way. The script needs jq.

Codex and other agents don't read .claude/settings.json, but the git hook still runs for them, because git runs it. For those, put the rule in AGENTS.md ("never use --no-verify; fix what the pre-commit hook reports") and keep the git hook as the part that enforces it.

If a key already got committed#

Removing the file in a new commit does not remove the key: it's still in the history, and in every clone and fork made since. Rotate the key at the provider first (revoke it and make a new one), then clean the history if the repository is shared. Run gitleaks git with no flags to scan the whole history for anything else that slipped in earlier.

Where Joxo fits#

When the hook stops an agent's commit, the agent often needs you: which variable name to use, or whether the value is real. With Joxo, that question reaches your phone and desktop, and you answer it from there instead of finding the stalled session later. Setup is one line pasted into your agent. For keeping agents away from .env files in the first place, see Keep Claude Code from reading your .env file; if you pasted a key into a chat with your agent, see Pasted an API key into Claude Code? Do this now.

Frequently asked questions#

Does a pre-commit hook work with Claude Code and Codex?#

Yes. Git runs the hook for every commit, whatever program runs git commit. Claude Code, Codex, Cursor and your own commits all go through it.

Can a coding agent skip a git pre-commit hook?#

Yes, with git commit --no-verify or -n. In Claude Code, block that with a deny rule and a PreToolUse hook that looks for the flag anywhere in the command. For other agents, say it in AGENTS.md.

Will gitleaks slow down commits?#

Not noticeably. With --staged it scans only the staged changes; our test took 25 ms.

What if gitleaks flags a fake key in a test?#

Add the comment gitleaks:allow to that line, or list the finding's fingerprint in a .gitleaksignore file at the root of the repository.