|
| 1 | +# With AI coding agents |
| 2 | + |
| 3 | +A coding agent commits more often than a person, and when a hook rejects a |
| 4 | +commit it can reach for `git commit --no-verify` to get past it. Agents that |
| 5 | +work in the cloud often run no hook at all. Four layers keep the policy in |
| 6 | +place, cheapest first. Only the last one cannot be skipped. |
| 7 | + |
| 8 | +## Tell the agent |
| 9 | + |
| 10 | +Put the rule where agents read their instructions. `AGENTS.md` at the |
| 11 | +repository root is read by Codex, Cursor, Jules, Devin, the Copilot coding |
| 12 | +agent and [many others](https://agents.md/). Claude Code reads it too when the |
| 13 | +repository has no `CLAUDE.md`; if it has one, add `@AGENTS.md` to it. |
| 14 | + |
| 15 | +```markdown title="AGENTS.md" |
| 16 | +## Commits |
| 17 | + |
| 18 | +Commit messages and branch names follow `cchk.toml`. When the commit-check |
| 19 | +hook fails, fix the message and commit again. Never pass `--no-verify` to |
| 20 | +`git commit` or `git push`, or `-n` to `git commit`. |
| 21 | +``` |
| 22 | + |
| 23 | +For more than one line of guidance, install the |
| 24 | +[agent skill](https://github.com/commit-check/commit-check/blob/main/skills/commit-check/SKILL.md). |
| 25 | +It teaches the agent to read `cchk.toml`, check a message before it commits, |
| 26 | +follow the [AI attribution policy](policies.md) and never skip the hook. It |
| 27 | +uses the [Agent Skills](https://agentskills.io) format, which Claude Code, |
| 28 | +GitHub Copilot, Codex, Cursor and others load: |
| 29 | + |
| 30 | +```console |
| 31 | +$ npx skills add https://github.com/commit-check/commit-check --skill commit-check |
| 32 | +``` |
| 33 | + |
| 34 | +With the [MCP server](mcp.md) the agent can also check the message as a tool |
| 35 | +call; [Make the agent use it](mcp.md#make-the-agent-use-it) has the line to add |
| 36 | +for that. |
| 37 | + |
| 38 | +An instruction is read, not enforced: the agent can still decide to skip the |
| 39 | +hook. |
| 40 | + |
| 41 | +## Block `--no-verify` |
| 42 | + |
| 43 | +Claude Code runs a [PreToolUse hook](https://code.claude.com/docs/en/hooks) |
| 44 | +before each shell command, and a hook that exits with code 2 stops the command |
| 45 | +and tells the agent why. This one stops `--no-verify`, and `-n` on |
| 46 | +`git commit`: |
| 47 | + |
| 48 | +```bash title=".claude/hooks/no-verify.sh" |
| 49 | +#!/usr/bin/env bash |
| 50 | +# Stop git from skipping the hooks: --no-verify, or -n on git commit. |
| 51 | +# Quoted text becomes _ first, so a commit message that mentions -n passes. |
| 52 | +command -v jq >/dev/null || { echo "This hook needs jq." >&2; exit 2; } |
| 53 | +cmd=$(jq -r '.tool_input.command' | sed -E "s/\"[^\"]*\"/_/g; s/'[^']*'/_/g") |
| 54 | +any='git [^;&|]*--no-verify' |
| 55 | +commit='git( +-C +[^ ]+)* +commit[^;&|]* +-[a-zA-Z]*n' |
| 56 | + |
| 57 | +if grep -Eq -e "$any" -e "$commit" <<<"$cmd"; then |
| 58 | + echo "Do not skip the hooks (--no-verify, -n)." \ |
| 59 | + "Fix what Commit Check reported and commit again." >&2 |
| 60 | + exit 2 |
| 61 | +fi |
| 62 | +``` |
| 63 | + |
| 64 | +```json title=".claude/settings.json" |
| 65 | +{ |
| 66 | + "hooks": { |
| 67 | + "PreToolUse": [ |
| 68 | + { |
| 69 | + "matcher": "Bash", |
| 70 | + "hooks": [ |
| 71 | + { |
| 72 | + "type": "command", |
| 73 | + "command": "\"${CLAUDE_PROJECT_DIR:-.}\"/.claude/hooks/no-verify.sh" |
| 74 | + } |
| 75 | + ] |
| 76 | + } |
| 77 | + ] |
| 78 | + } |
| 79 | +} |
| 80 | +``` |
| 81 | + |
| 82 | +Make the script executable, install [jq](https://jqlang.org/) (without it the |
| 83 | +hook stops every command), and commit both files, so everyone who works in the |
| 84 | +repository gets the hook. It reads the command as written and does not catch |
| 85 | +every way around a hook, such as a command wrapped in `bash -c` or |
| 86 | +pre-commit's `SKIP` variable. |
| 87 | + |
| 88 | +The same files work in other agents. [Cursor](https://cursor.com/docs/reference/third-party-hooks) |
| 89 | +loads hooks from `.claude/settings.json` (on by default) and treats exit code 2 |
| 90 | +the same way. [GitHub Copilot CLI](https://docs.github.com/en/copilot/reference/hooks-reference) |
| 91 | +reads `.claude/settings.json` in the repository too. The |
| 92 | +`${CLAUDE_PROJECT_DIR:-.}` form falls back to the repository root in an agent |
| 93 | +that does not set that variable. |
| 94 | + |
| 95 | +## Check before the agent hands over |
| 96 | + |
| 97 | +A blocked `--no-verify` only matters where a commit hook runs. A cloud agent |
| 98 | +such as the Copilot coding agent commits in a fresh sandbox, where your |
| 99 | +pre-commit hooks were never installed. A stop hook closes that gap: when the |
| 100 | +agent is about to finish, it runs Commit Check on every commit of the branch |
| 101 | +and, if one fails, sends the agent back to fix it. |
| 102 | + |
| 103 | +```bash title=".claude/hooks/commit-check-stop.sh" |
| 104 | +#!/usr/bin/env bash |
| 105 | +# Before the agent finishes, check every commit it made on this branch. |
| 106 | +# Asks the agent to fix them once per HEAD, so it cannot loop forever. |
| 107 | +command -v commit-check >/dev/null || exit 0 |
| 108 | +base=$(git merge-base HEAD origin/HEAD 2>/dev/null || |
| 109 | + git merge-base HEAD origin/main 2>/dev/null) || exit 0 |
| 110 | +marker="$(git rev-parse --git-dir)/commit-check-stop" |
| 111 | +[ "$(cat "$marker" 2>/dev/null)" = "$(git rev-parse HEAD)" ] && exit 0 |
| 112 | + |
| 113 | +report="" |
| 114 | +for sha in $(git rev-list --no-merges "$base"..HEAD); do |
| 115 | + out=$(commit-check --message --rev "$sha" --compact 2>&1) |
| 116 | + [ $? -eq 1 ] && report+="$(git log -1 --format='%h %s' "$sha")"$'\n'"$out"$'\n' |
| 117 | +done |
| 118 | +[ -z "$report" ] && exit 0 |
| 119 | + |
| 120 | +git rev-parse HEAD >"$marker" |
| 121 | +jq -cn --arg r "Commit Check failed on these commits: |
| 122 | +$report |
| 123 | +Reword them so they pass, then finish." '{decision: "block", reason: $r}' |
| 124 | +``` |
| 125 | + |
| 126 | +Only a failed check (exit code 1) sends the agent back. A config error, a |
| 127 | +missing `commit-check` or a branch with no `origin` lets it finish, and the |
| 128 | +pull request check below still applies. If the agent stops again without |
| 129 | +changing its commits, the hook lets it go. |
| 130 | + |
| 131 | +For Claude Code and Cursor, add a `Stop` entry next to `PreToolUse` in |
| 132 | +`.claude/settings.json`: |
| 133 | + |
| 134 | +```json title=".claude/settings.json" |
| 135 | +{ |
| 136 | + "hooks": { |
| 137 | + "PreToolUse": [ … ], |
| 138 | + "Stop": [ |
| 139 | + { |
| 140 | + "hooks": [ |
| 141 | + { |
| 142 | + "type": "command", |
| 143 | + "command": "\"${CLAUDE_PROJECT_DIR:-.}\"/.claude/hooks/commit-check-stop.sh" |
| 144 | + } |
| 145 | + ] |
| 146 | + } |
| 147 | + ] |
| 148 | + } |
| 149 | +} |
| 150 | +``` |
| 151 | + |
| 152 | +The Copilot coding agent reads only `.github/hooks/*.json` from the |
| 153 | +repository, and `agentStop` is its name for the same event: |
| 154 | + |
| 155 | +```json title=".github/hooks/commit-check.json" |
| 156 | +{ |
| 157 | + "version": 1, |
| 158 | + "hooks": { |
| 159 | + "agentStop": [ |
| 160 | + { |
| 161 | + "type": "command", |
| 162 | + "bash": "./.claude/hooks/commit-check-stop.sh" |
| 163 | + } |
| 164 | + ] |
| 165 | + } |
| 166 | +} |
| 167 | +``` |
| 168 | + |
| 169 | +The agent needs `commit-check` in its environment. For the Copilot coding |
| 170 | +agent, install it in the setup steps it runs before it starts: |
| 171 | + |
| 172 | +```yaml title=".github/workflows/copilot-setup-steps.yml" |
| 173 | +name: Copilot Setup Steps |
| 174 | +on: workflow_dispatch |
| 175 | +jobs: |
| 176 | + copilot-setup-steps: # Copilot looks for this exact job name |
| 177 | + runs-on: ubuntu-latest |
| 178 | + permissions: |
| 179 | + contents: read |
| 180 | + steps: |
| 181 | + - uses: actions/setup-python@v7 |
| 182 | + with: |
| 183 | + python-version: '3.x' |
| 184 | + - run: pip install commit-check |
| 185 | +``` |
| 186 | +
|
| 187 | +## Check the pull request |
| 188 | +
|
| 189 | +Hooks in the agent are feedback; the agent or a person can still get a bad |
| 190 | +commit to the pull request. The check that holds for every agent is the one |
| 191 | +on the pull request. The [GitHub App](github-app.md) needs no workflow file |
| 192 | +and also checks the agents that commit as an app |
| 193 | +([which ones](github-app.md#bots-re-runs-and-commits-checked-twice)); the |
| 194 | +[Action](github-actions.md) does the same job in CI. Make the check required |
| 195 | +and a commit that skipped the hook still cannot merge, since both look at every |
| 196 | +commit of the pull request. The App's check is named **Commit Check** |
| 197 | +([enforcing it](github-app.md#enforcing-it)); the Action's takes its job's |
| 198 | +name, `commit-check` in [the example](github-actions.md#making-it-a-required-check). |
0 commit comments