Tools like team and adhoc (from the mux-relay framework) derive the project root from $PWD at invocation time. The natural instinct is to open a new tmux window, cd to the target directory, then run the command. That works, but it leaves a ghost window in the original session — and if that window gets cleaned up or the session reshuffles, pane working directories can silently drift to deleted paths.
A subshell avoids all of that:
(j my-project && team)
The parentheses spawn a subshell. Inside it, j (zoxide) changes directory to the project, then team launches the new session with the correct $PWD. Because new-session -d is detached and switch-client handles the jump, the new session is fully independent — no nesting, no leftover windows.
When the subshell exits (right after team returns), the original pane's $PWD is untouched. You never left it.
This pattern works for any command that reads $PWD but doesn't accept a -C / --directory flag:
(cd ~/Projects/something && explore)
(cd $(z resolve another-project) && adhoc claude)
One pane, one command, zero side effects.
When you're staring at a 5000-line diff, your eyes glaze over before you've read past the first file. diffstat gives you the bird's-eye view in one screen:
gh pr diff 137 | diffstat
lib/relay-runtime.sh | 2692 -----------------------------------
lib/relay/backends.sh | 142 ++++
lib/relay/cli.sh | 326 +++++++
lib/relay/cycle.sh | 99 ++
lib/relay/issue-comments..| 179 ++++++
lib/relay/issues.sh | 173 ++++++
lib/relay/project-regist..| 274 ++++++++
lib/relay/session.sh | 692 ++++++++++++++++++++++++++
lib/relay/telegram.sh | 389 ++++++++++++++++
lib/relay/tmux-send.sh | 337 +++++++++++++++
lib/relay/worktree.sh | 206 ++++++++
11 files changed, 2848 insertions(+), 2686 deletions(-)
From that one output you can immediately see: this is a file split (one huge file → ten smaller ones), the net change is roughly neutral, and session.sh is the biggest new file at 692 lines. That's enough to form a review strategy before reading a single line of diff.
It also works with local diffs:
git diff main..my-branch | diffstat
Install on Debian/Ubuntu:
sudo apt install diffstat
The my-ai-team relay has a .my-ai-team/<mode>.md prompt override mechanism — drop a markdown file into a directory and it replaces the shipped system prompt for that agent mode. It never worked in practice. The code was there, the tests passed, but real users' override files were never found.
The bug was a root resolution mismatch, not a logic error.
The mechanism
_mux_prompt_override() in lib/relay-runtime.sh decides which prompt file an agent gets:
relay_root="$(_mux_relay_root)"
override_path="${relay_root}/.my-ai-team/${mode}.md"
It looks for .my-ai-team/<mode>.md under the relay root, then falls back to agents/<mode>.md.
What went wrong
_mux_relay_root() returns the install root (~/.local/share/my-ai-team/), not the project root. Meanwhile mux-agent-launch correctly receives MUX_PROJECT_ROOT (the user's project directory) and even cds into it — but _mux_prompt_override() never sees it.
Users put .my-ai-team/ in their project. The code only looks in the install dir. Ship mismatch.
Two problems stacked
- Project root never checked — the override path only searches the install root
- Install wipes install-root overrides —
install.sh does a full directory swap on every run (prepare_install_root_swap renames the old install root, moves staging in, deletes backup). A .my-ai-team/ manually placed at the install root is destroyed on reinstall
The fix: move global overrides out of managed territory
Instead of trying to preserve user data inside the install root (which install.sh owns and swaps), put global overrides somewhere install.sh never touches:
Resolution order:
1. ${MUX_PROJECT_ROOT}/.my-ai-team/<mode>.md — project-level (highest priority)
2. ~/.config/my-ai-team/<mode>.md — user-global (XDG config)
3. ${relay_root}/agents/<mode>.md — shipped default
No install.sh changes needed. The relay root stays a clean install-managed directory. User data lives in ~/.config/, which is user-owned by convention.
Why tests didn't catch it
The existing tests in test/prompt-override_test.sh test _mux_prompt_override() in isolation — they set up a fake relay root with .my-ai-team/ under it and verify the function finds it. The tests pass because the function's internal logic is correct. What's wrong is the root that gets passed in at runtime: the function is called with the install root when it should also check the project root. Tests that mock only one root won't catch a missing root.
You try to rename a directory and get blocked:
# Git Bash
$ mv my-ai-team my-ai-team.old.1
mv: cannot move 'my-ai-team' to 'my-ai-team.old.1': Permission denied
# PowerShell, even elevated
PS> Move-Item .\my-ai-team .\my-ai-team.old.1
Move-Item: You do not have sufficient access rights to perform this operation
or the item is hidden, system, or read only.
The instinct is to reach for chown/takeown/icacls. On Windows that's usually the wrong tree to bark up. When even an administrator shell is denied, the cause is almost never the ACL — it's a runtime lock: some process has a file inside the directory open, or has the directory itself as its current working directory. Windows surfaces that lock as "Access denied," which reads like a permissions problem but isn't.
Rule out permissions in two commands
…more
If you enabled Tailscale's built-in SSH server with tailscale up --ssh and then realized you'd rather just use OpenSSH with a real key (no periodic browser auth to Tailscale, fewer moving parts), the way you turn it off matters.
Use this:
sudo tailscale set --ssh=false
It flips that one flag and nothing else. tailscale status stops listing the SSH service, OpenSSH is untouched, your existing tailnet connection keeps running.
The trap is reaching for tailscale up --ssh=false. up is sticky — it reapplies your full login configuration — so it can prod you to re-authenticate in the browser, and any other flags you tweaked but forgot about get reset to defaults too. set is the surgical knob for toggling a single option without disturbing the rest.
Same shape applies to other booleans Tailscale exposes (--advertise-exit-node, --accept-routes, --shields-up): once the node is up and authenticated, prefer tailscale set --flag=... over re-running tailscale up.