From 85a0f1c421793dd0319d2838d985641e925fcb57 Mon Sep 17 00:00:00 2001 From: JacobPEvans <20714140+JacobPEvans@users.noreply.github.com> Date: Mon, 25 May 2026 18:48:40 -0400 Subject: [PATCH 1/2] fix(git-guards): enforce kernel coding-assistants spec for trailers and robot signature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Expand TRAILER_PATTERN to match bare `Assisted-by: Claude` (no model) in addition to the existing email form `Assisted-by: Claude <...>`; already-correct `Assisted-by: Claude:` is explicitly excluded - Add robot-signature stripping: removes `šŸ¤– Generated with [Claude Code]` lines (and the preceding blank line) from any Bash command — commits, PR body heredocs, etc. Blank-line variant replaced with \n to keep heredoc EOF delimiters on their own lines - Restructure main() so robot strip and trailer fix are independent paths; robot strip is transcript-independent (never fails open) - Update reason string to report which transforms were applied - Add 5 new tests: bare trailer rewrite, robot line in commit, robot line in gh pr create, both combined, robot strip without transcript Refs: https://docs.kernel.org/process/coding-assistants.html Assisted-by: Claude:claude-opus-4-7 --- git-guards/scripts/commit-trailer-guard.py | 52 ++++++++++---- .../scripts/test_commit_trailer_guard.py | 68 +++++++++++++++++++ 2 files changed, 107 insertions(+), 13 deletions(-) diff --git a/git-guards/scripts/commit-trailer-guard.py b/git-guards/scripts/commit-trailer-guard.py index 9b97108..0da8b8e 100755 --- a/git-guards/scripts/commit-trailer-guard.py +++ b/git-guards/scripts/commit-trailer-guard.py @@ -1,9 +1,12 @@ #!/usr/bin/env python3 """ -commit-trailer-guard.py - PreToolUse hook to rewrite Assisted-by trailer to kernel spec. +commit-trailer-guard.py - PreToolUse hook to enforce kernel coding-assistants spec. -Detects `Assisted-by: Claude <...>` in git commit commands and rewrites to -`Assisted-by: Claude:` per https://docs.kernel.org/process/coding-assistants.html. +Per https://docs.kernel.org/process/coding-assistants.html: + Correct: Assisted-by: Claude:claude-opus-4-7 + Wrong: Assisted-by: Claude (email form) + Wrong: Assisted-by: Claude (bare, no model) + Stripped: šŸ¤– Generated with [Claude Code](...) (not part of spec) """ import json @@ -11,9 +14,17 @@ import sys from pathlib import Path -TRAILER_PATTERN = re.compile(r"Assisted-by:\s*Claude\s*<[^>]*>") +# Matches the email form and the bare form, but NOT the already-correct Agent:model form. +TRAILER_PATTERN = re.compile(r"Assisted-by:\s*Claude(?:\s*<[^>]*>|(?!\s*:\S))") TRAILER_REPL = "Assisted-by: Claude:{model}" +_ROBOT_URL_PATTERN = r"šŸ¤– Generated with \[Claude Code\]\([^)]*\)" +# When the robot line is preceded by a blank line, replace the pair with a single newline +# so the text before it still ends cleanly (e.g. heredoc EOF stays on its own line). +_ROBOT_DOUBLE_NL = re.compile(r"\n\n" + _ROBOT_URL_PATTERN + r"\n?") +# When the robot line has no preceding blank, remove it entirely. +_ROBOT_SINGLE = re.compile(r"(?:\n|^)" + _ROBOT_URL_PATTERN + r"\n?") + # Matches git global flags that take a value (-C/-c) to strip before subcommand detection. _GIT_GLOBAL_VALUE = re.compile(r'^-[Cc]\s+(?:"[^"]*"|\'[^\']*\'|\S+)\s*') # Matches valueless git global flags (-p/-P/--paginate/etc.). @@ -79,28 +90,43 @@ def main() -> None: tool_input = data.get("tool_input", {}) command = tool_input.get("command", "") - if not TRAILER_PATTERN.search(command): - sys.exit(0) + is_commit = command.startswith("git ") and _is_git_commit(command) + needs_trailer_fix = is_commit and bool(TRAILER_PATTERN.search(command)) + needs_robot_strip = bool(_ROBOT_DOUBLE_NL.search(command) or _ROBOT_SINGLE.search(command)) - if not command.startswith("git ") or not _is_git_commit(command): + if not needs_trailer_fix and not needs_robot_strip: sys.exit(0) - model = _get_model_from_transcript(data.get("transcript_path", "")) - if not model: - sys.exit(0) + model = "" + if needs_trailer_fix: + model = _get_model_from_transcript(data.get("transcript_path", "")) + if not model: + needs_trailer_fix = False + + new_command = command + if needs_robot_strip: + new_command = _ROBOT_DOUBLE_NL.sub("\n", new_command) + new_command = _ROBOT_SINGLE.sub("", new_command) + if needs_trailer_fix: + new_command = TRAILER_PATTERN.sub(TRAILER_REPL.format(model=model), new_command) - new_command = TRAILER_PATTERN.sub(TRAILER_REPL.format(model=model), command) if new_command == command: sys.exit(0) + reason_parts = [] + if needs_trailer_fix: + reason_parts.append(f"trailer rewritten (model={model})") + if needs_robot_strip: + reason_parts.append("robot-signature line stripped") + print(json.dumps({ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "allow", "permissionDecisionReason": ( - f"Trailer rewritten to kernel coding-assistants spec " + "kernel coding-assistants spec enforced " f"(https://docs.kernel.org/process/coding-assistants.html): " - f"model={model}" + + ", ".join(reason_parts) ), }, "updatedInput": {**tool_input, "command": new_command}, diff --git a/git-guards/scripts/test_commit_trailer_guard.py b/git-guards/scripts/test_commit_trailer_guard.py index 7484855..fa7f2ea 100755 --- a/git-guards/scripts/test_commit_trailer_guard.py +++ b/git-guards/scripts/test_commit_trailer_guard.py @@ -16,7 +16,9 @@ MODEL = "claude-opus-4-7" OLD_TRAILER = "Assisted-by: Claude " +BARE_TRAILER = "Assisted-by: Claude" NEW_TRAILER = f"Assisted-by: Claude:{MODEL}" +ROBOT_LINE = "šŸ¤– Generated with [Claude Code](https://claude.com/claude-code)" def _make_transcript(model: str = MODEL) -> str: @@ -133,6 +135,72 @@ def check(label: str, command: str, expected_decision: str, transcript_path=transcript, ) +# 8. Bare trailer (no email, no model) → rewritten to Agent:model +cmd8 = f'git commit -m "fix: x\n\n{BARE_TRAILER}"' +all_pass &= check( + "bare trailer rewrite", + cmd8, + "allow", + transcript_path=transcript, + expected_new_command=cmd8.replace(BARE_TRAILER, NEW_TRAILER), +) + +# 9. Robot line preceded by blank in commit (already-correct trailer) → robot stripped. +# The blank-line replacement (\n\nšŸ¤–... → \n) leaves a trailing \n inside the message. +cmd9 = f'git commit -m "fix: x\n\n{NEW_TRAILER}\n\n{ROBOT_LINE}"' +expected9 = f'git commit -m "fix: x\n\n{NEW_TRAILER}\n"' +all_pass &= check( + "robot line stripped from commit", + cmd9, + "allow", + transcript_path=transcript, + expected_new_command=expected9, +) + +# 10. Robot line in gh pr create body → stripped (not a git command). +# \n\nšŸ¤–...\n → \n keeps the heredoc EOF on its own line. +pr_cmd = ( + f'gh pr create --title "feat: x" --body "$(cat <<\'EOF\'\n' + f'## Summary\n- did a thing\n\n' + f'{ROBOT_LINE}\n' + f'EOF\n)"' +) +expected10 = ( + f'gh pr create --title "feat: x" --body "$(cat <<\'EOF\'\n' + f'## Summary\n- did a thing\n' + f'EOF\n)"' +) +all_pass &= check( + "robot line stripped from gh pr create", + pr_cmd, + "allow", + transcript_path=transcript, + expected_new_command=expected10, +) + +# 11. Bare trailer + robot line together → both fixed in one pass. +# Robot strip runs first, leaving trailing \n; then trailer fix rewrites BARE → NEW. +cmd11 = f'git commit -m "fix\n\n{BARE_TRAILER}\n\n{ROBOT_LINE}"' +expected11 = f'git commit -m "fix\n\n{NEW_TRAILER}\n"' +all_pass &= check( + "bare trailer + robot line fixed together", + cmd11, + "allow", + transcript_path=transcript, + expected_new_command=expected11, +) + +# 12. Robot line + missing transcript → robot still stripped (robot strip is transcript-independent). +cmd12 = f'git commit -m "fix: x\n\n{ROBOT_LINE}"' +expected12 = 'git commit -m "fix: x\n"' +all_pass &= check( + "robot line stripped even without transcript", + cmd12, + "allow", + transcript_path="", + expected_new_command=expected12, +) + print() print("ALL TESTS PASSED" if all_pass else "SOME TESTS FAILED") sys.exit(0 if all_pass else 1) From 10c11ff339b1dbc7809df732b56d531ff52e730b Mon Sep 17 00:00:00 2001 From: JacobPEvans <20714140+JacobPEvans@users.noreply.github.com> Date: Mon, 25 May 2026 18:55:28 -0400 Subject: [PATCH 2/2] fix(git-guards): add word boundary to prevent false matches on Claude-prefixed names Without \b after "Claude", the TRAILER_PATTERN incorrectly matched names like "Assisted-by: Claudette <...>" via the negative-lookahead path. Add \b so only the standalone word "Claude" matches. Also add test 13: verifies "Claudette" is NOT rewritten (silent allow). Assisted-by: Claude:claude-opus-4-7 --- git-guards/scripts/commit-trailer-guard.py | 3 ++- git-guards/scripts/test_commit_trailer_guard.py | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/git-guards/scripts/commit-trailer-guard.py b/git-guards/scripts/commit-trailer-guard.py index 0da8b8e..5a1a14d 100755 --- a/git-guards/scripts/commit-trailer-guard.py +++ b/git-guards/scripts/commit-trailer-guard.py @@ -15,7 +15,8 @@ from pathlib import Path # Matches the email form and the bare form, but NOT the already-correct Agent:model form. -TRAILER_PATTERN = re.compile(r"Assisted-by:\s*Claude(?:\s*<[^>]*>|(?!\s*:\S))") +# \b after Claude prevents false matches on names like "Claudette", "Claudine", etc. +TRAILER_PATTERN = re.compile(r"Assisted-by:\s*Claude\b(?:\s*<[^>]*>|(?!\s*:\S))") TRAILER_REPL = "Assisted-by: Claude:{model}" _ROBOT_URL_PATTERN = r"šŸ¤– Generated with \[Claude Code\]\([^)]*\)" diff --git a/git-guards/scripts/test_commit_trailer_guard.py b/git-guards/scripts/test_commit_trailer_guard.py index fa7f2ea..fad4609 100755 --- a/git-guards/scripts/test_commit_trailer_guard.py +++ b/git-guards/scripts/test_commit_trailer_guard.py @@ -201,6 +201,15 @@ def check(label: str, command: str, expected_decision: str, expected_new_command=expected12, ) +# 13. Name that starts with "Claude" (Claudette) must NOT be rewritten — word-boundary guard. +cmd13 = 'git commit -m "fix: x\n\nAssisted-by: Claudette "' +all_pass &= check( + "name starting with Claude is not rewritten", + cmd13, + "silent_allow", + transcript_path=transcript, +) + print() print("ALL TESTS PASSED" if all_pass else "SOME TESTS FAILED") sys.exit(0 if all_pass else 1)