Skip to content

Global agent-context block asserts "You are running inside wmux" in every session on the machine, including ones that aren't #152

Description

@capad-xyz

Summary

wmux writes its auto-managed block into three global agent-context files:

Agent File Written by
Claude Code ~/.claude/CLAUDE.md ensureClaudeContext()
OpenCode ~/.config/opencode/AGENTS.md ensureOpencodeContext()
Kiro ~/.kiro/steering/wmux.md ensureKiroContext()

All three receive the same file, resources/claude-instructions.md, byte-for-byte. It opens with:

# wmux
You are running inside wmux, a terminal multiplexer with a browser panel on the right side that the user can see in real-time.

You are running inside wmux, a terminal multiplexer with a browser panel on the right side that the user can see in real-time.

and then, on the strength of that claim, forbids the agent's own web tooling:

For any web browsing task, use the `wmux browser` commands so the user can watch in the browser panel. Do NOT use Playwright, Firecrawl, or WebSearch — they open invisible windows the user cannot see. If the user explicitly asks for one of those tools, use it.

For any web browsing task, use the wmux browser commands so the user can watch in the browser panel. Do NOT use Playwright, Firecrawl, or WebSearch — they open invisible windows the user cannot see.

Because the file is global, it loads in every session on the machine — not only the ones inside wmux. Claude Code Desktop, a plain terminal, a headless or scheduled run, CI, an SSH session: all of them read ~/.claude/CLAUDE.md. In those sessions the opening sentence is false, and the instruction that follows is unfollowable — wmux may not even be on PATH, and if wmux is not running the CLI cannot reach the pipe.

What actually happens

Observed on v0.46.0 (Windows 11), 2026-08-12. A Claude Code Desktop session — no wmux process running at all — read the block, believed it, and tried to drive wmux browser for a web task while explicitly avoiding WebSearch. It burned several turns on a tool that could not work before noticing the contradiction and falling back. The failure mode is quiet: the agent is not confused about the task, it is confidently following instructions that were true somewhere else.

The instruction is unusually load-bearing for a false premise, because it does not merely offer wmux browser — it removes the alternative. An agent that believes it has no other web tool has no obvious reason to go looking for one.

Worth noting this is not limited to Claude Code: the same text lands in OpenCode's global AGENTS.md and, since #148, in Kiro's global steering directory, which Kiro CLI loads unconditionally for every workspace.

Root cause

The claim is treated as a write-time fact when it is really a read-time one.

ensureClaudeContext() runs when wmux starts, and at that instant "wmux is running" is true — so the sentence is accurate about the moment it is written:

export function ensureClaudeContext(): void {
try {
const instructionsPath = getInstructionsPath();
if (!fs.existsSync(instructionsPath)) {
console.warn('[wmux] claude-instructions.md not found at', instructionsPath);
return;
}
const wmuxBlock = fs.readFileSync(instructionsPath, 'utf-8');
const claudeMdPath = getClaudeMdPath();
const claudeDir = path.dirname(claudeMdPath);
// Ensure ~/.claude/ exists
if (!fs.existsSync(claudeDir)) {
fs.mkdirSync(claudeDir, { recursive: true });
}
if (!fs.existsSync(claudeMdPath)) {
// No CLAUDE.md yet — create with just the wmux block
fs.writeFileSync(claudeMdPath, wmuxBlock, 'utf-8');
console.log('[wmux] Created ~/.claude/CLAUDE.md with wmux context');
return;
}

But the file it writes is then read by every future session on the machine, arbitrarily later, most of which have no relationship to that wmux instance. Nothing re-evaluates the claim at the point where it is consumed, and nothing can: the writer has no way to know who will read the file or where from.

Why this is awkward to fix, and what I'd like your call on

I can see three shapes and they trade off differently against decisions you have already made deliberately, so I would rather ask than guess.

1. Make the block self-checking (my preference). Keep the global file, but stop asserting presence as fact. Open with a cheap verification step (wmux ping) and say what to do in both outcomes — use wmux browser when it answers, fall back to the agent's native web tools when it does not.

  • It fixes all three agents with one edit, because all three share the one content file.
  • It respects the "one source of truth, no per-agent drift" rule you set out in kiro-context.ts.
  • No new machinery, no install-time behaviour change, no consent-model change.
  • Cost: a few lines of prose, and it spends a subprocess call on first web use.

2. Stop writing to the global file. Cleanest in principle, largest change — and I think parts of it are already ruled out by decisions you have made:

  • Per-project is out by your own reasoning: writing into every repository the user opens "is precisely the complaint in wmux modifies global claude settings without permission and/or warning #132" (kiro-context.ts).
  • Per-session env injection does not address the reported case at all. The sessions that get this wrong are exactly the ones wmux did not spawn, so there is no shell of ours to inject into.
  • That leaves dropping global context entirely, which costs the discoverability the current design is clearly buying.

3. Make the content conditional at generation time. I don't think this can work, for the reason in the root-cause section: the condition is read-time. wmux would be baking "wmux is running" into a file read by sessions that start hours later, and the answer can differ per reader. It would still be wrong for the desktop-app case, which is the one that actually bit.

So my read is that 1 is the only option that fixes the reported failure without giving up something you chose on purpose — but you own the discoverability/verbosity trade-off here, and if you would rather go a different way I'm happy to implement whichever you pick.

Aside

wmux ping is a good probe for option 1 specifically: it is answered before the auth gate in handleV1, so it needs no pipe token, and it fails fast when nothing is listening.

if (command === 'ping') {
socket.write('pong\n');
return;
}
if (!authed) {
socket.write('unauthorized\n');
return;
}


Happy to open the PR once you've picked a direction. If you want option 1, I'd keep the diff to resources/claude-instructions.md plus a test pinning that the block never asserts wmux's presence unconditionally.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions