tmux: Hold Alt and Spam B to Cycle Sessions

The standard way to switch to the previous tmux session is prefix + L (or a custom prefix + b). The problem: every switch requires the full prefix chord again. You can't hold Ctrl and keep tapping — tmux swallows the first keypress as the prefix and expects a fresh command key each time.

Use a no-prefix binding instead. Add to ~/.tmux.conf:

# Alt+b to switch to previous session (no prefix needed).
bind -n M-b switch-client -p

The -n flag means "no prefix required." Now you hold Alt and tap b repeatedly to cycle through sessions — no prefix dance, no finger gymnastics.

Why Alt+B and not something else: it's close to the default prefix (Ctrl+B) so it's easy to remember, and it doesn't conflict with any common terminal or shell binding. Your mileage may vary if you use Alt-heavy terminal apps.

Launch a new tmux session from the right directory — without polluting the current one

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.

diffstat: the first thing to reach for when reviewing a big PR

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

When your "extensible" feature never works: trace the root, not just the hook

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

  1. Project root never checked — the override path only searches the install root
  2. Install wipes install-root overridesinstall.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.

"Permission denied" renaming a folder on Windows is usually an open handle, not a permission

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