Repository navigation
Description
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/resources/claude-instructions.md
Lines 3 to 5 in fae21a5
| # 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:
wmux/resources/claude-instructions.md
Line 9 in fae21a5
| 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 browsercommands 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:
wmux/src/main/claude-context.ts
Lines 32 to 54 in fae21a5
| 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.
Lines 152 to 160 in fae21a5
| 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
fix(context): have the agent check for wmux instead of asserting it (#…
amirlehmam commentedon Aug 13, 2026
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 pingfor the reason you gave — answered before the auth gate inhandleV1, 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 preferwmux browserbecause 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:ensureClaudeContextfinds 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/# smuxH1 whose body carries one of wmux's own sentences (a user's own# wmuxnotes survive), it tracks fenced code so a# Titleinside an example cannot cut a span short, and it excludes the managed block — which has the same heading — from the scan entirely. Ten tests intests/unit/agent-context-block.test.tscover it, including idempotency and the fenced-heading case.Tests pinning the block itself
wmux pingbefore the first section that depends on the answerThank 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.
fix(context): stop reading "wmux: command not found" as "wmux is abse…
fix(pack): ship the canonical agent instructions and the app icon