No description.

Blocks a git commit whose message has a Co-Authored-By: trailer, for any co-author. The model then removes the trailer and runs the commit again, because you do not want attribution lines in commits.
One matcher, core.ts, serves three agents through two adapters. An adapter is the small file that connects the matcher to the hook API of one agent.
| Agent | Adapter | Loaded by |
|---|---|---|
| Claude Code | hooks/register.ts, a mod: tool.call on Bash returns { deny } | CLAUDE_CODE_PLUGIN_DIRS in settings.json.tmpl |
| pi and omp | pi.ts: tool_call on bash returns { block, reason } | extensions in dot_pi/agent/modify_settings.json and dot_omp/private_agent/private_config.yml |
Codex does not run this hook. See docs/hooks/decisions.md.
The hook runs before each shell call: Bash in Claude Code, bash in pi and omp. It blocks the call when both of these are true, case-insensitive:
commit is a whole word in the command. A letter, a digit, _, or - next to it makes a different word, so git log --grep 'Co-Authored-By: Claude', rg ... committed.txt, and git commit-tree pass.co-authored-by:. The hook does not read the name after it, so it blocks every agent, also one that does not exist yet, with no list to keep current.The hook also blocks a human co-author. In an agent's commit, a human trailer occurs only when you ask for one, or when the agent rewrites a message that already has one. In those cases, commit by hand.
pi.ts in their own runtime, so the hook needs no node on PATH.pi.ts ignores other tools and a command that is not a string.violation() in core.ts tests the command for the two conditions in What it checks.The check reads the whole command, so the hook also blocks a command that commits and only mentions the trailer. An example is a git commit and a cat of a file that holds the trailer, joined with &&. To run such a command, split it into two calls.
One gap remains: a message written to a file in an earlier call and passed with git commit -F <file> passes. A git commit-msg hook is the upgrade path.
When the hook blocks a call, the command does not run. The model gets this block message as the reason:
Blocked: remove the Co-Authored-By trailer from the commit message and run the commit again. The user does not want attribution lines in commits.
The hook runs in the agent process, so it has no exit code. A call that passes gives the model no output.
The hook has no variables or config files. AGENT_HOOKS_DISABLE does not turn it off, because it is a guard, not an advisory hook. See toggle-hook.
To turn the hook off:
claude-nohooks or another launch mode that sets disableAllHooks. These also turn off this mod (tested on 2026-10-19). See launch-modes.md.pi.ts line of this hook from extensions in the agent's settings.pi.ts does no I/O and catches nothing. If it throws on a malformed event, pi blocks that call, so the hook fails closed.node test/check.ts checks the matcher and both adapters. See docs/testing.md.
attribution.commit: "" in settings.json stops Claude Code from adding the trailer. It does not stop the model from writing the trailer itself, out of habit.
A permissions.deny rule such as Bash(git commit Co-Authored-By: Claude) blocks the -m form. A test on Claude Code 2.1.291 showed that it does not match the trailer in a heredoc body (git commit -F - <<'EOF'). The model uses that form most.
| File | Deployed to ~/.claude/hooks/block-commit-attribution/ | Role |
|---|---|---|
core.ts | Yes | Matcher and block message. No I/O. |
hooks/register.ts, hooks/hooks.json | Yes | Claude Code mod |
dot_claude-plugin/plugin.json | Yes, as .claude-plugin/plugin.json | Plugin manifest |
pi.ts | Yes | pi and omp extension |
README.md, docs/, test/ | No | Kept in the repo by .chezmoiignore |
hooks/register.ts 16 lines1/**
2 * Claude Code mod: denies a Bash call that commits with a
3 * `Co-Authored-By:` trailer. Fails open: a hook that throws is skipped
4 * and the call runs, as the command hook did.
5 */
6
7import type { Register } from "claude-code";
8import { violation } from "../core.ts";
9
10export const register: Register = (on) => {
11 on("tool.call", { tool: "Bash" }, async (_$, e, next) => {
12 const reason = violation(e.command);
13 return reason ? { deny: reason } : next(e);
14 });
15};
16core.ts 29 lines1/**
2 * The block-commit-attribution matcher: finds a `git commit` whose message
3 * has a `Co-Authored-By:` trailer. Pure: the adapters pass the command line
4 * in.
5 *
6 * Adapters: hooks/register.ts (Claude Code mod), pi.ts (pi and omp).
7 */
8
9// Any co-author, so a new agent needs no change. A human co-author is blocked
10// too: the user then commits by hand. Agents write Co-Authored-By and
11// Co-authored-by.
12const TRAILER = /co-authored-by:/i;
13// `commit` as a whole word, so `git log --grep 'Co-Authored-By: Claude'` and
14// `rg ... committed.txt` pass.
15const COMMIT = /(^|[^A-Za-z0-9_-])commit([^A-Za-z0-9_-]|$)/i;
16
17/** The reason the model reads when the call is blocked. */
18export const MESSAGE =
19 "Blocked: remove the Co-Authored-By trailer from the commit message and run the commit again. The user does not want attribution lines in commits.";
20
21/**
22 * The block message when the line commits with a co-author trailer, else
23 * undefined. shortcut: a message written to a file first and passed with
24 * `git commit -F <file>` passes; a git commit-msg hook is the upgrade path.
25 */
26export function violation(line: string): string | undefined {
27 return COMMIT.test(line) && TRAILER.test(line) ? MESSAGE : undefined;
28}
29