8000 Global agent-context block asserts "You are running inside wmux" in every session on the machine, including ones that aren't · Issue #152 · amirlehmam/wmux · GitHub
[go: up one dir, main page]

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

Closed

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.

Activity

  1. amirlehmam commented on Aug 13, 2026

    @amirlehmam
    Owner

    Option 1, shipped in 0.50.0. Your framing settled it: the claim is a write-time fact being read at an arbitrarily later time by someone else, so nothing at generation time can make it true — it has to be checkable by its reader. And you're right that option 2 is mostly foreclosed by decisions already made; the per-project variant is #132 again, and env injection cannot reach the sessions that get this wrong, since those are exactly the ones wmux did not spawn.

    The block now opens by naming its own scope and handing the reader a probe:

    wmux writes this section into a global agent-context file, so every session on this machine loads it — including sessions that are not running inside wmux at all […] Whether it applies to you is a fact about your session, so check it rather than assume it:

    wmux ping     # "pong" → wmux is here; anything else → it is not

    If it does not answer […] Ignore the whole section and use your normal tools. Nothing here is a restriction on what you may otherwise do.

    wmux ping for the reason you gave — answered before the auth gate in handleV1, no token, fails fast on nothing listening. One file, so Claude, OpenCode and Kiro all get it.

    The browser section changed too, and this is the load-bearing edit. The old text did not merely offer wmux browser, it removed the alternative ("Do NOT use Playwright, Firecrawl, or WebSearch"), and an agent that believes it has no other web tool has no reason to look for one. It now says prefer wmux browser because the user can watch the panel, and says outright to use your own tools when the check says wmux is absent.

    What the fix turned up

    Writing the test you suggested — pin that the block never asserts presence unconditionally — meant reading a real ~/.claude/CLAUDE.md. It had the wmux block six times: five # wmux, one # smux.

    Copies written before the block was marker-delimited (and before the rename) have no <!-- wmux:start -->. Splicing keys off the first marker pair, so those copies were never updated and never removed: ensureClaudeContext finds the newest, rewrites it, and steps over the rest. They accumulate one per rename or marker change and load into every session on the machine forever — several of them contradicting each other about the CLI's own name, which is its own small version of the problem you reported.

    So stripLegacyBlocks() now runs before the splice, on the Claude and OpenCode paths both. It only takes a # wmux / # smux H1 whose body carries one of wmux's own sentences (a user's own # wmux notes survive), it tracks fenced code so a # Title inside an example cannot cut a span short, and it excludes the managed block — which has the same heading — from the scan entirely. Ten tests in tests/unit/agent-context-block.test.ts cover it, including idempotency and the fenced-heading case.

    Tests pinning the block itself

    • never states as fact that the reader is inside wmux
    • names wmux ping before the first section that depends on the answer
    • says what to do when the check fails
    • never removes the reader's alternatives outright
    • all three agents are still written from the one content file, so this cannot drift per-agent

    Thank you for asking rather than guessing, and for laying out which options were already ruled out by decisions in the repo — that is what made this a five-minute call rather than a design discussion. The PR offer was appreciated; I folded it in directly since the legacy-block cleanup landed in the same function.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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

      0