Skip to content

Commit 2a6bf7b

Browse files
authored
docs: add a guide on keeping AI coding agents to the commit policy (#70)
1 parent 971fe23 commit 2a6bf7b

3 files changed

Lines changed: 204 additions & 2 deletions

File tree

‎docs/guides/ai-agents.md‎

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
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).

‎docs/guides/github-app.md‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,11 @@ behind the target the branch is.
6565
### Pull requests
6666

6767
Every commit of a pull request is checked on its own, fork pull requests
68-
included, and pushing more commits checks the new ones. A team that
69-
squash-merges keeps none of those commits, so it can check the result instead:
68+
included, and pushing more commits checks the new ones. GitHub reads a
69+
required check on the newest commit only, so that commit's check also fails
70+
when an earlier commit of the pull request failed, and lists the commits to
71+
fix. A team that squash-merges keeps none of those commits, so it can check the
72+
result instead:
7073

7174
```toml title="cchk.toml"
7275
[pull_request]

‎mkdocs.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ nav:
125125
- GitHub App: guides/github-app.md
126126
- GitLab, Bitbucket, Azure: guides/other-ci.md
127127
- With release tools: guides/release-tools.md
128+
- With AI coding agents: guides/ai-agents.md
128129
- Across an organization: guides/organization.md
129130
- MCP server: guides/mcp.md
130131
- Python API: guides/python-api.md

0 commit comments

Comments
 (0)