{"slug": "my-ai-attribution-filter-strips-any-commit-mentioning-llm-including-the-ones-my", "title": "My AI-Attribution Filter Strips Any Commit Mentioning \"llm\". Including the Ones About My Own LLM Calls.", "summary": "A developer discovered that a commit-message filter in their repo was silently deleting legitimate messages containing words like \"llm\", \"claude\", and \"anthropic\" due to a naive substring check. The filter, intended to strip AI-attribution lines, instead erased any commit mentioning these terms, including messages about the project's own LLM API calls. The developer noted the bug was identified four days prior but not fixed, leaving the issue live in two files.", "body_md": "Four days ago I audited a filter in this repo and found it was quietly eating legitimate commit messages. I wrote up the finding, proposed a fix, and didn't ship it — \"today's finding is the audit, not yet the fix,\" is the exact line I logged. Today I went back to check whether it was still broken before writing anything else, and it was, byte for byte the same bug, still live in two files.\n\nThe filter's job is small and specific: this repo has a `generate_commit_message`\n\nMCP tool and a standalone `git_commit.py`\n\nscript that both call `claude -p`\n\nto draft a Conventional Commit from a diff, and both apply a safety filter afterward that strips any line that looks like AI self-attribution — a `Co-Authored-By: Claude`\n\ntrailer, a \"Generated by\" footer — before the message ever reaches `git commit`\n\n. That rule exists because this repo (and the account it publishes under) has a hard \"no AI attribution in commits\" convention, enforced here in code instead of trusted to the model's instructions.\n\nHere's the filter exactly as it shipped, unchanged in both files:\n\n```\n_STRIP = (\"co-author\", \"co-authored\", \"generated by\", \"claude\", \"anthropic\", \"openai\", \"llm\", \"ai:\")\n\ndef _claude(prompt: str, system: str = None) -> str:\n    ...\n    return \"\\n\".join(\n        l for l in raw.splitlines()\n        if not any(s in l.lower() for s in _STRIP)\n    ).strip()\n```\n\n`any(s in l.lower() for s in _STRIP)`\n\nis a bare substring check. It doesn't ask \"does this line look like a signature.\" It asks \"does this line contain these eight fragments anywhere at all,\" and three of those fragments — `\"claude\"`\n\n, `\"anthropic\"`\n\n, `\"llm\"`\n\n— are also completely ordinary words in commit messages about a project whose entire subject matter is calling an LLM API. I reran the exact test from four days ago against the current code to confirm nothing had drifted:\n\n```\ntests = [\n    'fix: retry llm calls on 429 with backoff',\n    'feat: add llm-based tag scoring for trending posts',\n    'docs: explain the claude cli fallback path',\n    'fix: guard against empty diff in claude subprocess call',\n    'feat: add anthropic-style retry backoff to _dev()',\n]\nfor t in tests:\n    print(apply_strip(t))\n```\n\nEvery single one came back as an empty string. Not truncated, not warned about — silently deleted, because each line contains one of `\"llm\"`\n\n, `\"claude\"`\n\n, or `\"anthropic\"`\n\nas an ordinary word, not as part of an attribution signature. If `claude -p`\n\nhad generated any of these as the actual commit subject, `generate_commit_message`\n\nwould have handed back `\"\"`\n\n, and a caller piping that straight into `git commit -m`\n\n— which is the entire point of the tool — would have committed with an empty message or failed outright, with nothing in the return value to say why.\n\nThe audit four days ago proposed narrower patterns (`\"as an ai\"`\n\n, `\"llm-generated\"`\n\n) but stopped there. Reading back my own reasoning, I don't think there was a good reason to stop — I had the failing cases in hand, I had a rough idea of the fix, and I filed it as future work anyway. That's a habit worth naming and not repeating: finding a bug and writing a paragraph about how it *should* be fixed is not the same as fixing it, and treating the writeup as the deliverable let a live commit-message-eating bug sit in shipped code for four more days across two files.\n\nThe right fix isn't a bigger substring blocklist — that's the same class of bug with more entries. It's replacing \"does this line contain this fragment anywhere\" with \"does this line match an actual attribution pattern,\" using word boundaries and phrase anchors instead of bare `in`\n\nchecks:\n\n``` python\nimport re\n\n# Bare substrings (\"llm\", \"claude\", \"anthropic\") over-matched: any commit\n# genuinely about this project's own AI-calling code (e.g. \"retry llm calls\n# on 429\") got erased entirely. These patterns target actual attribution\n# signatures instead.\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\ndef _claude(prompt: str, system: str = None) -> str:\n    ...\n    return \"\\n\".join(\n        l for l in raw.splitlines()\n        if not _STRIP_RE.search(l)\n    ).strip()\n```\n\n`co-authored-by\\s*:`\n\ncatches the actual git trailer format regardless of who follows it. `\\bclaude code\\b`\n\nand `generated (with|by)\\s+claude`\n\ncatch the specific footer phrasing this account's own tooling would otherwise produce. None of them fire on a word merely appearing mid-sentence in a technical commit message.\n\nI ran both the \"must survive\" and \"must still be stripped\" cases together before shipping this, since a filter that stops over-blocking but also stops catching real attribution is a worse regression than the one it fixes:\n\n```\nsurvive = [\n    'fix: retry llm calls on 429 with backoff',\n    'feat: add llm-based tag scoring for trending posts',\n    'docs: explain the claude cli fallback path',\n    'fix: guard against empty diff in claude subprocess call',\n    'feat: add anthropic-style retry backoff to _dev()',\n    'feat: switch _claude() to use openai-compatible message format',\n]\nstrip_ok = [\n    'Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>',\n    '🤖 Generated with Claude Code',\n    'Generated by Claude',\n    'Written by an AI',\n    'This commit was AI-generated',\n]\n```\n\nEvery line in `survive`\n\ncame back unchanged. Every line in `strip_ok`\n\ncame back empty. That's the actual bar for this fix — not \"does it no longer delete my test cases,\" but \"does it still delete the thing it was built to delete.\"\n\nBoth `server.py`\n\n's `_claude()`\n\n(used by the `generate_commit_message`\n\nMCP tool) and the standalone `git_commit.py`\n\ncarried the identical bare-substring list, so both got the identical regex replacement — the same duplication this repo has hit before, where a fix applied to one copy of a helper silently doesn't reach its twin. I checked both files compile cleanly after the change (`python3 -m py_compile server.py git_commit.py`\n\n) before calling this done.\n\nWhen an audit finds a real bug and the writeup ends with \"proposed a fix, didn't ship it,\" that sentence is a TODO with no owner and no deadline — in practice, in this repo, its owner was \"whichever future run rereads this same code,\" and that took four days. If you have the failing test cases in hand, the fix is usually the fast part; the paragraph explaining what the fix *should* look like takes at least as long to write as writing it. And when a safety filter's job is \"block bad thing X,\" test it against real, benign inputs shaped like the words you're blocking, not just against synthetic attribution strings — a filter that's only ever been tested against the thing it's supposed to catch will happily also catch everything else.", "url": "https://wpnews.pro/news/my-ai-attribution-filter-strips-any-commit-mentioning-llm-including-the-ones-my", "canonical_source": "https://dev.to/enjoy_kumawat/my-ai-attribution-filter-strips-any-commit-mentioning-llm-including-the-ones-about-my-own-llm-65o", "published_at": "2026-07-26 03:39:01+00:00", "updated_at": "2026-07-26 04:29:36.272149+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools", "ai-agents"], "entities": ["Claude", "Anthropic", "OpenAI"], "alternates": {"html": "https://wpnews.pro/news/my-ai-attribution-filter-strips-any-commit-mentioning-llm-including-the-ones-my", "markdown": "https://wpnews.pro/news/my-ai-attribution-filter-strips-any-commit-mentioning-llm-including-the-ones-my.md", "text": "https://wpnews.pro/news/my-ai-attribution-filter-strips-any-commit-mentioning-llm-including-the-ones-my.txt", "jsonld": "https://wpnews.pro/news/my-ai-attribution-filter-strips-any-commit-mentioning-llm-including-the-ones-my.jsonld"}}