My agents got their own config. Two still read mine.

Builds on My benchmark read my CLAUDE.md and Claude Code read my home folder as a project, which explain two of the leaks below in detail.

I keep a set of config templates for six terminal coding agents: OpenCode in two major versions, pi, omp, Hermes Agent and Claude Code. They're set up to run on local models and to block destructive commands even when nobody is approving them. Anyone installing them may already have a config of their own for at least one of those agents, so the installer has a mode for that. I call it installing apart: the template goes into its own directory, ~/.config/harnesses/<name>, a small launcher points the agent there, and your own config is never touched. That is the whole promise, one clause long, and I wrote it.

Never touched was true. It was also the wrong promise. Installing apart separates config files; it does nothing about what each agent reads by convention, and two agent families went to my home directory anyway. One of them also sent a prompt off the machine. My ~/.claude is the folder where my personal Claude Code instructions, rules and skills live, and I'd like to say I was surprised to find it read. I have lost count of the posts in which it turns up where it shouldn't. This time it got past the installer whose whole job is keeping the two apart, and the agents didn't have to break the promise to do it. They just read it more carefully than I had.

Claude Code: a 64k context, full before the first answer#

Claude Code 2.1.314's first run with a real model was installed apart, with CLAUDE_CONFIG_DIR pointing at the template's directory. The model was a 35B mixture-of-experts coding model at 64k context, served by llama.cpp. The task was small on purpose: a Go Sum function, a table test, then go test and go run.

The session ended after 13 turns with no file written and terminal_reason: rapid_refill_breaker. Autocompact is Claude Code summarising the conversation to make room when the context fills, and the message said it had stopped trying: "Autocompact is thrashing: the context refilled to the limit within 3 turns of the previous compact, 3 times in a row." I have rarely agreed with an error message so completely.

The first request was 34,758 input tokens. The transcript lists the instruction files Claude Code loaded, in three parts:

  • the template's own CLAUDE.md, 5.6k characters;
  • the project's CLAUDE.md;
  • labelled as Project memory: my ~/.claude/CLAUDE.md, 26.6k characters, plus 16 files from ~/.claude/rules/.

That last part is about 45k characters written for my own Claude Code setup, including a rule to push after every commit and notes about my machines. A model that came to write a Sum function was first briefed on my git habits.

The mechanism is the one from part 17 of the bench series, and it's documented: Claude Code loads CLAUDE.md, .claude/CLAUDE.md and .claude/rules/*.md from the working directory and from every directory above it. Your home directory is above every project, so ~/.claude/ is found again as some ancestor's .claude/ folder. CLAUDE_CONFIG_DIR relocates the user configuration; the walk up the tree never consults it. The variable does exactly what its name says, and not one directory more.

Why it thrashed instead of just running slow#

The numbers leave no room. 64k of context, minus a 16k reserve for output, minus a buffer, puts the autocompact threshold near 36k. The base prompt alone was about 35k. Every compaction made room, and the fixed part of the prompt filled it straight back up.

The fix is the setting the docs offer for exactly this, claudeMdExcludes: glob patterns matched against absolute paths, accepted in any settings layer, with arrays from different layers merged. Whenever the launcher runs an install apart, it now adds three patterns to its session settings: ~/.claude/CLAUDE.md, ~/.claude/CLAUDE.local.md and ~/.claude/rules/**.

The rerun's first request was 22,560 tokens, 12,198 fewer. The task finished in 6 turns and 18 seconds, tests green.

NoteTwo changes, one rerun

The rerun also added --permission-mode acceptEdits. Headless claude -p without it refuses every write ("Claude requested permissions to write ... but you haven't granted it yet"), and the first run had been looping on denied writes as well as thrashing. The token count shows the exclusion took effect; both changes were needed for the green run.

So the exclusion gets credit for the 12,198 tokens, not for the whole rescue. The launcher's new unit check also fails against the old launcher script, so it would have caught the old behaviour; a test that passes on the old code tells me nothing.

A marker in every place I didn't want read#

The check for the rest is cheap, and by then I had stopped taking any template's word for where its agent reads:

  • a throwaway HOME with marker files where my personal setup would live: ~/.claude/CLAUDE.md containing MARKER_CLAUDE_MD, a ~/.claude/rules/marker.md, and a skill at ~/.claude/skills/marker-skill/SKILL.md;
  • every agent installed apart into that home;
  • a fake OpenAI-compatible model server that writes every request body to a log;
  • the prompt "say hi", then a grep of the log for the markers.
AgentWhat reached the fake model
OpenCode V1 1.19.5the marker skill; not the CLAUDE.md
OpenCode V2 2.2.0no request at all (next section)
pino marker
omp 18.13.3no marker
Hermes Agentreads only the project's CLAUDE.md and guards against home-level ones; it looks in ~/.claude/ only for Anthropic credentials, not used here

Claude Code is the section above.

OpenCode V1: the CLAUDE.md lost on a technicality#

OpenCode reads a global rules file from the first of two places that exists: <config>/AGENTS.md, then ~/.claude/CLAUDE.md. The template ships its own AGENTS.md, so my CLAUDE.md lost. Skills have no such tie-break, and my ~/.claude/skills loaded anyway.

Part 5 of the bench series found the same skill loading in benchmark prompts. I turned the compatibility off in the bench. The templates didn't get the memo until a marker file asked.

The OpenCode binary has three switches for this: OPENCODE_DISABLE_CLAUDE_CODE, the broad one; OPENCODE_DISABLE_CLAUDE_CODE_PROMPT; and OPENCODE_DISABLE_CLAUDE_CODE_SKILLS, which also skips a project's .claude/skills. The OpenCode launchers now set the skills one to 1 unless you set it yourself, and =0 keeps your skills. The rerun sent no marker. Measured on a separate test machine's setup, the Claude Code compatibility had cost +3.5k prompt tokens and +32 seconds per fresh session.

OpenCode V2: zero requests, and an answer anyway#

OpenCode V2 sent zero requests to the fake server. Then it answered "say hi" anyway, from somewhere else.

It came from flint-40b-free. V1, run with no model server at all, quietly went online too and answered from big-pickle. Both are OpenCode's own hosted free models, under the provider id opencode, and they need no API key. Either way, the prompt left the machine. I have never been happier that a test prompt was boring.

V2 was worse than that. With the old template, and the local fake model up and discovered, it still picked the hosted model.

V2 reads provider rules only from policies#

For V1 the fix is one key:

{ "disabled_providers": ["opencode"] }

V2 has permissions and policies, and the two sound interchangeable right up until one of them is ignored. The same rule as a permission entry, {"action": "provider.use", ...} under permissions, had no effect. The binary showed why: V2 evaluates provider.use only from policies, and it converts the legacy disabled_providers key into provider.use deny entries there. Moved to policies, the rule works. Under permissions, it had promised a deny and delivered a comment.

The check was a count of messages from the hosted provider in OpenCode's session database. The old templates had 13 (V1) and 1 (V2); the new ones have 0. With no local model the run now fails with "No model is available for session ...", which is the failure I want: loud, local, and on nobody else's server. With the local model up, V2 now uses it.

The small_model title pin: a guard for a closed road#

One more thing fell out of turning the hosted provider off. The V1 discovery plugin had pinned small_model, the model OpenCode uses for session titles, to the default model, for a stated reason: "so titles never go to a hosted model". That was a fine reason until the hosted provider was gone.

Without the reason, only the cost was left. A session started on another model with -m .../devstral still sent its title request to the pinned one, and on a model server that keeps one model loaded at a time, that's a model swap per session to name a conversation. A guard rail for a road I'd just closed, now standing in the middle of the next one. Unset, the title goes to the session's own model; I checked that against the fake server with 1.19.5.

What installing apart can't separate#

The Claude Code leak showed up in a real run's transcript. The other two came from the cheaper check: a marker in each place I don't want read, a fake model that logs what it's sent, and grep. Claude Code walks up the directory tree as documented, OpenCode reads Claude Code's skills as its compatibility docs describe (quoted in part 5), and its hosted models are a feature for people who want them. A separate config directory doesn't reach any of it. None of it is a bug, which leaves me nobody to be cross with except the person who wrote "never touched" and assumed it covered reading.

The installer still promises that your own config is never touched, and it keeps that promise to the letter. My ~/.claude will turn up somewhere it shouldn't again; it has a talent for it. Next time I'd like it found by a file that says MARKER_CLAUDE_MD, not by a coding model reading my rules about git.

Filed under: ai-agents, claude-code, opencode, local-llm

Sources#

  • Claude Code read my home folder as a project: the Claude Code memory documentation on instruction files above the working directory and on claudeMdExcludes, quoted and linked there.
  • Session artifacts (local, not published): the Claude Code transcripts and instruction-file list from both runs, the marker probe's request log, OpenCode V1 and V2 session databases, the launcher and its unit check.