{"slug": "my-ai-attribution-filter-stopped-over-matching-ordinary-words-it-still-wipes-any", "title": "My AI-Attribution Filter Stopped Over-Matching Ordinary Words. It Still Wipes Any Commit That's Legitimately About Claude Code.", "summary": "A developer fixed a bug in an AI-attribution filter that used bare substring matching, which accidentally erased legitimate commit messages containing the word 'llm'. After replacing the substrings with anchored regexes, the filter became more precise but still incorrectly strips any commit that legitimately mentions 'Claude Code' as a technical noun, such as 'docs: add claude code hook install instructions'. The filter's per-line logic combined with the generator's one-line output can delete entire commit messages, causing silent failures.", "body_md": "This repo has a git hook that generates commit messages with `claude -p`\n\n, then runs the output through a filter that strips anything that looks like AI self-attribution — no \"Co-Authored-By: Claude,\" no \"🤖 Generated with Claude Code\" footer, nothing that violates the project's no-AI-attribution rule. A few weeks ago I found and fixed a real bug in that filter: it used bare substring matching, so a legitimate commit like `fix: retry llm calls on 429 with backoff`\n\ngot silently erased just for containing the word \"llm.\" I replaced the bare substrings with anchored regexes and moved on, confident the filter was now precise.\n\nIt's more precise. It's still wrong, in the opposite direction, for one specific pattern.\n\nThe current filter lives in both `server.py`\n\nand `git_commit.py`\n\n(same logic, two files — a known drift risk in this repo I've written about before):\n\n```\n_STRIP_PATTERNS = [\n    r\"co-authored-by\\s*:\",\n    r\"generated (with|by)\\s+claude\",\n    r\"\\bclaude code\\b\",\n    r\"\\bwritten by (an )?(ai|llm|claude|chatgpt|copilot)\\b\",\n    r\"\\bai-generated\\b\",\n    r\"🤖\",\n]\n_STRIP_RE = re.compile(\"|\".join(_STRIP_PATTERNS), re.IGNORECASE)\n```\n\nAuditing this file for the audit-log bug I wrote about separately, I re-ran the fixed regex against realistic inputs instead of trusting that \"anchored now\" meant \"correct now.\" The line I stopped on was `r\"\\bclaude code\\b\"`\n\n. It's a bare phrase match — no requirement that \"Claude Code\" appear as part of an attribution statement. And this project's own commit history is full of legitimate reasons to say \"Claude Code\" as a plain technical noun: it's the name of the CLI tool this repo's hooks, scripts, and MCP server all wrap.\n\n```\ncandidates = [\n    \"fix: retry llm calls on 429 with backoff\",\n    \"docs: add claude code hook install instructions\",\n    \"feat: wire up claude code review workflow for PRs\",\n    \"chore: document claude code slash commands in README\",\n    \"fix: handle claude code MCP timeout in server.py\",\n]\nfor c in candidates:\n    print(bool(_STRIP_RE.search(c)), \"|\", c)\nFalse | fix: retry llm calls on 429 with backoff\nTrue  | docs: add claude code hook install instructions\nTrue  | feat: wire up claude code review workflow for PRs\nTrue  | chore: document claude code slash commands in README\nTrue  | fix: handle claude code MCP timeout in server.py\n```\n\nFour out of five candidates get stripped, and none of them are attribution. They're ordinary descriptions of work on this repo's own git hook and MCP tooling — the exact kind of commit this repo generates constantly, since half its work log is about fixing `hooks/prepare-commit-msg`\n\n, `scripts/install-hooks.sh`\n\n, and the `claude -p`\n\nwrapper itself.\n\nThe generator's system prompt tells `claude -p`\n\nto output exactly one line — no explanation, no markdown, just the commit message. The filter operates per-line:\n\n```\nmsg = \"\\n\".join(\n    l for l in raw.splitlines()\n    if not _STRIP_RE.search(l)\n).strip()\n```\n\nWhen the output is genuinely multi-line, stripping one offending line still leaves the rest of the message intact. But the system prompt guarantees there's only ever one line to begin with. If that single line matches `_STRIP_RE`\n\nfor any reason — including a false positive — `msg`\n\nbecomes the empty string. There's no partial redaction, no fallback line, nothing. `hooks/prepare-commit-msg`\n\nchecks `[ -n \"$MSG\" ]`\n\nbefore writing anything to the commit editor, so an empty message here is indistinguishable from \"the AI call produced nothing\" or \"the hook isn't installed\" — the exact failure mode two earlier bug-log entries already covered for different root causes. This bug produces the same silent symptom through a third mechanism: a working hook, a working `claude -p`\n\ncall, a real one-line commit message, deleted in full because it happened to describe the tool by name.\n\nMy first instinct was that `r\"\\bclaude code\\b\"`\n\nlooks redundant with `r\"generated (with|by)\\s+claude\"`\n\n— surely \"Generated with Claude Code\" already matches the second pattern. I checked before assuming that:\n\n``` python\nimport re\np = re.compile(r\"generated (with|by)\\s+claude\", re.I)\nprint(bool(p.search(\"🤖 Generated with [Claude Code](https://claude.ai/code)\")))\n```\n\n`False`\n\n. The real attribution footer Claude Code emits is markdown-linked — `[Claude Code](url)`\n\n— and the bracket sits between \"with\" and \"Claude,\" breaking the no-punctuation assumption in the existing pattern. The bare `\\bclaude code\\b`\n\nline isn't dead weight; it's the only pattern in the list that actually catches the bracketed real-world footer. Deleting it would have reopened the exact hole the filter exists to close, just to fix the false positive I'd found. That's not a fix, that's trading one bug for the other.\n\nScope the match to require an attribution preposition immediately before \"claude code,\" and allow the bracket:\n\n```\nr\"\\b(with|by|using|via)\\s*\\[?\\s*claude code\\]?\",\n```\n\nReran both sides of the test:\n\n``` php\n🤖 Generated with [Claude Code](https://claude.ai/code)  -> True  (still caught)\nCo-Authored-By: Claude <noreply@anthropic.com>             -> True  (still caught)\ngenerated by claude code                                   -> True  (still caught)\ndocs: add claude code hook install instructions             -> False (no longer stripped)\nfeat: wire up claude code review workflow for PRs            -> False (no longer stripped)\nchore: document claude code slash commands in README         -> False (no longer stripped)\nfix: handle claude code MCP timeout in server.py              -> False (no longer stripped)\n```\n\nEvery real attribution shape I tested still gets caught. Every legitimate technical mention now survives. Applied the same one-line change to both `server.py`\n\nand `git_commit.py`\n\n, since they share the pattern list by copy, not by import — the same twin-drift shape that's bitten this repo before, so I checked both files instead of just the one I started reading.\n\n\"I anchored the regex, so it's precise now\" and \"I anchored the regex correctly\" are different claims, and only the second one is worth writing a bug-log entry closed. A blocklist filter guarding against a specific bad phrase needs testing against two directions, not one: does it still catch the bad phrase in its real, messy form (markdown brackets included), and does it now let through the ordinary vocabulary of the exact project it's running in. I'd tested the first direction when I shipped the anchored-regex fix. I hadn't tested the second, and this repo's own commit history is the one place where \"the tool's own name\" and \"a phrase that means AI wrote this\" are guaranteed to collide.", "url": "https://wpnews.pro/news/my-ai-attribution-filter-stopped-over-matching-ordinary-words-it-still-wipes-any", "canonical_source": "https://dev.to/enjoy_kumawat/my-ai-attribution-filter-stopped-over-matching-ordinary-words-it-still-wipes-any-commit-thats-2mn3", "published_at": "2026-08-02 03:38:59+00:00", "updated_at": "2026-08-02 04:38:08.896294+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence"], "entities": ["Claude Code", "claude", "MCP", "server.py", "git_commit.py", "prepare-commit-msg"], "alternates": {"html": "https://wpnews.pro/news/my-ai-attribution-filter-stopped-over-matching-ordinary-words-it-still-wipes-any", "markdown": "https://wpnews.pro/news/my-ai-attribution-filter-stopped-over-matching-ordinary-words-it-still-wipes-any.md", "text": "https://wpnews.pro/news/my-ai-attribution-filter-stopped-over-matching-ordinary-words-it-still-wipes-any.txt", "jsonld": "https://wpnews.pro/news/my-ai-attribution-filter-stopped-over-matching-ordinary-words-it-still-wipes-any.jsonld"}}