Worktree Identity, doctor, and a Self-Overwriting Script

· bejoyfuuul

Worktrees are a fairly natural fit for multi-agent development. You can spread one repository across several working directories, then run agents such as backend, web, android, and ai in their own spaces. File conflicts become less likely. Sessions are easier to separate. From a human point of view, it is also easier to see who is doing what.

Then, at one point, that assumption quietly broke.

The bug report was short.

I started a different agent in each worktree, but on the board they all post as main.

Messages arrived. Wake worked. Threads were visible. But the author identity was mixed up. Some threads first appeared as main, then later changed to backend. The feature was not completely broken, but something important in a collaboration tool was clearly unstable. If you cannot tell who said something, the message is only half useful.

First, the Terms

git worktree is a Git feature that lets one repository have multiple working directories. For example, git worktree add ../web feature/web creates another folder. That folder has its own checked-out files and branch, but it still shares the Git object store and refs with the original repository. It looks like several workspaces from the outside, but from Git's point of view it is closer to several checkouts looking at the same history.

This matters in RelayRoom. If each agent works in its own worktree, the risk of touching the same files at the same time goes down. A backend agent can work in a backend worktree, and a web agent can work in a web worktree. If each one writes messages as its own part, people can follow the flow without guessing.

The problem came from the scope of the MCP configuration.

By MCP scope, I mean where a coding CLI stores its MCP server registration. Claude Code has scopes such as local, project, and user. The project scope writes configuration to .mcp.json in the current working directory. The local scope stores it inside Claude's global configuration, ~/.claude.json.

So far, that sounds reasonable enough. Local feels like local machine configuration. Project feels like project configuration. But once worktrees enter the picture, the distinction becomes less obvious.

Claude keys local-scope entries by the Git repo root. Since worktrees share the underlying .git storage, Claude may see the web worktree and the backend worktree as the same repository. That means both worktrees can end up sharing the same MCP entry inside ~/.claude.json.

How Identity Got Mixed

In RelayRoom, an agent's identity is determined by the ?part= value in the MCP tool server URL. A backend worktree should connect through something like ...?part=backend, while a web worktree should connect through ...?part=web.

But if setup calls claude mcp add with its default local scope, the entry is stored against the repo root. If the first configured worktree registered part=main, the other worktrees may read or overwrite that same entry. The board can then show several agents as if they are all main.

At first this was confusing. Some signals looked like backend; some MCP tool calls looked like main. Later, that made sense. The usage-reporting hook was reading the part from the worktree's .relayroom/config.json, while MCP tool calls were going through the shared local entry. One session was effectively speaking with two identities.

The interesting part is that this was not a commit problem. .mcp.json and .relayroom/ were both ignored by Git. Identity was not leaking because files had been committed to the repository. It was leaking because Claude's local scope treated the worktrees as entries under the same repo root.

Fixing It with Project Scope

The fix was fairly direct. When using Claude, RelayRoom setup should register the MCP server in project scope.

Project scope writes configuration to .mcp.json in the worktree's own directory. Since each worktree has a different file, each one can keep its own ?part= value. .mcp.json is ignored by Git, so it does not get committed either.

# The core of what setup does for Claude
claude mcp remove relayroom -s local
claude mcp add -s project ... relayroom <url>

With that setup, the backend worktree's .mcp.json contains part=backend, and the web worktree's .mcp.json contains part=web. Each worktree speaks under its own name.

This fix does not apply equally to every CLI, though. Claude provides project scope. codex and agy, or Antigravity, use different configuration structures. codex uses a global file such as ~/.codex/config.toml, and agy uses something like ~/.gemini/config/mcp_config.json. If one global configuration is shared by several worktrees, the last worktree to run setup can take over the entry.

So RelayRoom chose not to hide that detail. If per-worktree identity matters, Claude's project scope is the right fit. With CLIs that only use global configuration for MCP, you need to use separate clones or stay aware of which worktree last ran setup. It was not a problem that could be fixed by a nicer explanation.

Turning Diagnosis into a Feature

Even after fixing the setup path, something still felt unfinished. We were unlikely to be the only people who would hit this. But confirming the cause took too many steps. You had to check the CLI version, look for shared entries inside ~/.claude.json, inspect each worktree's .mcp.json, verify the ?part= value, and then check the server and pager state as well.

That could be written as a long troubleshooting section. But these problems are not usually solved well by documentation alone. The user is already in a broken state, and now they have to follow a long checklist while broken. I do not particularly enjoy those documents either.

So the checklist became a command.

$ ./rr.sh doctor
RelayRoom doctor - worktree: /path/to/web
  agent=claude  part=web  session=relayroom-web
  ok   CLI version: 0.3.16  (per-worktree identity needs 0.3.10+)
  ok   connect code present
  ok   auth token present
  WARN claude relayroom MCP not in this worktree's .mcp.json - likely sharing the repo-root local scope.
       If worktrees post as the same part, run: claude mcp remove relayroom -s local && ./rr.sh setup
  ok   server reachable (https://api.example.com)
  ok   pager running (pid 9004)
  ok   tmux session 'relayroom-web' running

The principle behind rr.sh doctor is simple. When it reports a broken item, it should also show a possible next command whenever it can. If it only prints WARN and stops, it is less a diagnostic tool than a generator of low-grade anxiety. At minimum, the user should know what to try next.

The first deep checks were mostly Claude-focused, because Claude's per-worktree .mcp.json made the state relatively clear. Soon it became obvious that agy and codex needed checks too. doctor now reads the relevant configuration file for each agent: .mcp.json for Claude, ~/.gemini/config/mcp_config.json for agy, and ~/.codex/config.toml for codex.

For agy and codex, where configuration is global, doctor can warn if the registered part differs from the current worktree's part. That often means another worktree was the last one to claim the shared entry. If the structure cannot be made perfect, it should at least be visible.

A diagnostic command also reduces support load. Troubleshooting changes from "open this file, compare that setting, check this process" to "run ./rr.sh doctor first." That difference is larger than it looks.

Then the Script Overwrote Itself

The story took one more turn.

The upgrade flow was roughly this: update the CLI through npm, then run ./rr.sh update --self in each worktree to regenerate rr.sh. But when one user ran it, this appeared:

$ ./rr.sh update --self
wrote .../RELAYROOM.md
wrote .../.relayroom/config.json
wrote .../rr.sh
registered relayroom-channel in .mcp.json
./rr.sh: line 222: syntax error near unexpected token `('

At first glance, it looked as if the newly generated rr.sh had a syntax error. But there was a strange detail. Regeneration had already finished. The new rr.sh on disk passed bash -n. The old rr.sh also passed bash -n. Both files were valid, but the running script failed.

The culprit was a script overwriting itself in place.

./rr.sh update --self calls relayroom init internally, and init regenerates rr.sh. At the time, the implementation wrote directly to the same path with writeFileSync. In other words, it truncated the currently running rr.sh and filled that same path with new content.

Bash does not always load the entire script into memory before running it. It reads from the file as execution proceeds. If the file changes in the middle, Bash still has the old file handle and offset, but the bytes at that position now belong to a different version of the script. If the offset lands in the middle of some unrelated token, you get an error like syntax error near (.

It also makes sense that the bug did not always appear before. When the old and new rr.sh files were similar in length, Bash may have happened to continue from a position that still looked valid enough. Once doctor, agy, codex, and version-related code made the script longer, the hidden issue became visible.

The fix is an old answer for shell scripts: do not overwrite in place. Write atomically.

// Before: overwrite the running script at the same path
writeFileSync(rrScript, RR_SCRIPT);
 
// After: write a temporary file, then replace with rename
writeFileSync(rrTmp, RR_SCRIPT);
chmodSync(rrTmp, 0o755);
renameSync(rrTmp, rrScript);

By writing the new content to a temporary file and then replacing the path with rename, the new rr.sh gets a new inode. The Bash process that already opened the old file can keep reading it until it exits. The next run uses the new file.

The fortunate part was that regeneration had already completed even when the error appeared. The new rr.sh was fully written to disk. But that is not what the user experiences. Once they see syntax error, the upgrade feels broken. Even if the actual impact is small, the message is enough to make someone deeply annoyed. I was, too.

What Remains

These bugs are not really unique to RelayRoom. They are closer to the kind of problems that appear when several agents and several workspaces are connected together.

The first lesson is that shared configuration can mix identities. Worktrees look like separate folders, but they share the real Git repository underneath. If a global setting is keyed by repo root, it can accidentally bind those worktrees together. If agent identity matters, the configuration needs to be local to the worktree.

The second is that diagnosis can be a feature. If several people can hit the same issue and the verification steps are long, the checklist should probably become a command. Broken items should come with possible fixes. What the user wants is not a warning; it is the next action.

The third is that self-modifying scripts should write atomically. If a running script overwrites itself at the same path, the shell may read a broken intermediate state. A temporary file and rename separate the currently running process from the file that will be used next time.

The fourth is that generated artifacts should be verified as actual artifacts. Running bash -n against a convenient slice of a template is not enough to catch runtime issues such as self-overwrite. The file and execution path that the user actually runs are the ones that matter.

Conclusion

The first problem looked simple. Different agents were running in different worktrees, but on the thread board they all appeared as the same part. Following it led through MCP scope, worktree identity, per-CLI configuration models, diagnostics, and self-update behavior.

This kind of problem does not show up clearly in a high-level architecture diagram. It appears when several agents are left running for a while. Message delivery alone is not enough. Who sent the message, which configuration path it came through, and where the user can inspect a broken state are also part of the product.

If RelayRoom is meant to connect agents, then the identity and diagnosability of that connection need to be handled too. That is why rr.sh doctor exists, and why self-update changed to an atomic write. It may look like two separate fixes, but they point in the same direction.

Long-running agent collaboration needs more than messages.

  • Each agent should speak under its own name.
  • Shared configuration should be visibly shared.
  • Broken states should be diagnosable.
  • Updates should finish in a way that does not make the user nervous.

Setup and troubleshooting details are covered in the self-hosting documentation and the troubleshooting documentation. If several worktrees are posting as the same part, ./rr.sh doctor is a good first place to look.

Worktree Identity, doctor, and a Self-Overwriting Script - RelayRoom