{"slug": "i-ran-claude-p-for-one-commit-message-my-whole-claude-md-came-along-uninvited", "title": "I Ran `claude -p` for One Commit Message. My Whole CLAUDE.md Came Along Uninvited.", "summary": "An engineer discovered that Anthropic's Claude CLI, when invoked with `claude -p` for a simple commit message generation task, silently loads the project's CLAUDE.md file from the current working directory, adding unnecessary tokens and coupling the script's behavior to external instructions. The engineer found that the `--bare` flag disables this auto-discovery but requires an API key, conflicting with projects that rely on OAuth sessions.", "body_md": "I have a 20-line script called `git_commit.py`\n\nthat reads `git diff --staged`\n\nand shells out to `claude -p`\n\nto turn it into a Conventional Commit message. It's about as narrow a task as an LLM call gets: one diff in, one line out. No file exploration, no tool use, nothing that should care what project it's running in.\n\n```\nraw = subprocess.check_output(\n    [\"claude\", \"-p\", SYSTEM + \"\\n\\n\" + diff],\n    text=True,\n    timeout=20,\n    stderr=subprocess.PIPE,\n).strip()\n```\n\nI only went looking at this because I was reviewing my repo's `CLAUDE.md`\n\nfor an unrelated reason and noticed it opens with a block the file itself calls \"MANDATORY routing rules\" — instructions about routing shell output through sandbox tools, blocking `curl`\n\n, indexing web pages before reading them, that kind of thing. None of it applies to a script whose entire job is \"here's a diff, give me a commit message.\" So I assumed it didn't matter: `git_commit.py`\n\ncalls `claude -p`\n\nwith a fully self-contained system prompt and a diff string. Why would it load a project instructions file at all?\n\nI checked instead of assuming. From the repo root, I ran the same shape of call `git_commit.py`\n\nmakes, with a prompt that just asks the model to say whether it saw those rules:\n\n``` bash\n$ claude -p \"Reply with exactly one line: either 'YES-SAW-CTX-RULES' if your \\\ncontext includes instructions mentioning ctx_fetch_and_index or context-mode \\\nMANDATORY routing rules, or 'NO-CTX-RULES' if it does not. Nothing else.\"\nYES-SAW-CTX-RULES\n```\n\nIt loaded it. A bare `claude -p`\n\ninvocation with no flags picks up `CLAUDE.md`\n\nfrom whatever directory it's launched in, the same auto-discovery an interactive session does, regardless of whether the prompt has anything to do with the project. `git_commit.py`\n\ndoesn't pass a `cwd`\n\nto `subprocess.check_output`\n\n, so it inherits the caller's — which for a script that's supposed to be run from inside the repo, is exactly this directory.\n\nThat file is 4,404 bytes, 79 lines. Roughly a thousand tokens of routing rules, output-format constraints, and a project memory-system protocol block, none of which a one-shot \"diff → commit message\" completion has any use for. It's not incorrect, exactly — the commit message that comes back is still fine. It's just a fixed cost, paid silently, on every single call, for content the task can't act on.\n\nThe part that actually worried me wasn't the token cost. It was the coupling. `git_commit.py`\n\n's behavior is nominally defined entirely by the `SYSTEM`\n\nstring hardcoded at the top of the file:\n\n```\nSYSTEM = (\n    \"You are a git commit message generator. \"\n    \"Output ONLY the commit message — one line, no explanation, no markdown, no quotes, \"\n    ...\n)\n```\n\nBut that's not actually true. Whatever `CLAUDE.md`\n\nhappens to exist in the cwd at call time rides along too, unannounced, and can just as easily change the output. If some future edit to this project's `CLAUDE.md`\n\nadded \"always write commit messages in title case\" or \"prefer sentence-style subjects,\" `git_commit.py`\n\nwould start doing that with zero code changes and zero visibility into why — the same script, the same hardcoded prompt, a different result depending entirely on which directory it happened to be launched from.\n\nSo I looked for the fix. `claude --help`\n\ndocuments exactly this scenario:\n\n```\n--bare    Minimal mode: skip hooks, LSP, plugin sync, attribution,\n          auto-memory, background prefetches, keychain reads, and\n          CLAUDE.md auto-discovery. Sets CLAUDE_CODE_SIMPLE=1. Anthropic\n          auth is strictly ANTHROPIC_API_KEY or apiKeyHelper via\n          --settings (OAuth and keychain are never read).\n```\n\nThat second sentence is the catch. This project's `key_facts.md`\n\nsays, in so many words: \"No ANTHROPIC_API_KEY — Claude calls go through `claude -p`\n\nsubprocess (OAuth session).\" `--bare`\n\nexplicitly refuses to read the OAuth session and demands an API key instead. Adding `--bare`\n\nto fix a context-loading problem would have broken the auth this script actually depends on — a fix that looks right in the docs and fails the moment you run it.\n\nThe flag that does what I actually wanted was `--safe-mode`\n\n:\n\n```\n--safe-mode   Start with all customizations (CLAUDE.md, skills, plugins,\n              hooks, MCP servers, custom commands and agents, output\n              styles, workflows, custom themes, keybindings, and more)\n              disabled ... Auth, model selection, built-in tools, and\n              permissions work normally.\n```\n\n\"Auth ... works normally\" is the line that matters here — it disables `CLAUDE.md`\n\ndiscovery without touching how the process authenticates. Verified the same way as before:\n\n``` bash\n$ claude -p --safe-mode \"Reply with exactly one line: either 'YES-SAW-CTX-RULES' ... or 'NO-CTX-RULES' ...\"\nNO-CTX-RULES\n```\n\nThat's the fix I shipped, in both places this repo makes the same kind of call — `git_commit.py`\n\n's subprocess invocation and `server.py`\n\n's `_claude()`\n\nhelper behind the `generate_commit_message`\n\nMCP tool:\n\n```\nraw = subprocess.check_output(\n    [\"claude\", \"-p\", \"--safe-mode\", SYSTEM + \"\\n\\n\" + diff],\n    text=True,\n    timeout=20,\n    stderr=subprocess.PIPE,\n).strip()\n```\n\nThe lesson isn't \"add `--safe-mode`\n\neverywhere.\" It's that a headless CLI call inherits ambient context the same way a shell inherits environment variables — silently, by default, scoped to wherever it's invoked from — and the flag that fixes that isn't always the first one that sounds like it should. `--bare`\n\nreads as the obvious answer to \"stop auto-loading project files,\" and for a setup authenticated with an API key it would be. For a setup that authenticates over OAuth, it's the one flag on that list that quietly breaks the thing you're trying to fix in the first place. The only way I found that out was running both and checking, not reading the flag name and assuming.", "url": "https://wpnews.pro/news/i-ran-claude-p-for-one-commit-message-my-whole-claude-md-came-along-uninvited", "canonical_source": "https://dev.to/enjoy_kumawat/i-ran-claude-p-for-one-commit-message-my-whole-claudemd-came-along-uninvited-3kb2", "published_at": "2026-08-10 03:38:33+00:00", "updated_at": "2026-08-10 04:20:34.964373+00:00", "lang": "en", "topics": ["developer-tools", "large-language-models", "ai-tools"], "entities": ["Anthropic", "Claude CLI"], "alternates": {"html": "https://wpnews.pro/news/i-ran-claude-p-for-one-commit-message-my-whole-claude-md-came-along-uninvited", "markdown": "https://wpnews.pro/news/i-ran-claude-p-for-one-commit-message-my-whole-claude-md-came-along-uninvited.md", "text": "https://wpnews.pro/news/i-ran-claude-p-for-one-commit-message-my-whole-claude-md-came-along-uninvited.txt", "jsonld": "https://wpnews.pro/news/i-ran-claude-p-for-one-commit-message-my-whole-claude-md-came-along-uninvited.jsonld"}}