Debugging session findings and fixes for the macOS iTerm2 + tmux -CC (native control-mode integration) setup. Covers where prefs actually live (and why the plist must be edited only while iTerm2 is quit), plus root-caused fixes: control-mode garbage dump, git error noise on login, aggressive-resize reverting, applied iTerm2 plist tweaks, the single -CC client per session limit, Claude Code tab naming, Agent Teams spawning broken nested -CC panes, and split-menu cwd inheritance. Reusable rules at the end.

Where Config Actually Lives

Sources of truth (edit these, not the defaults)

  • iTerm2 prefs: ~/.dotfiles/config/iterm2/com.googlecode.iterm2.plist — iTerm2 has LoadPrefsFromCustomFolder=1 pointing here, and NoSyncNeverRemindPrefsChangesLostForFile=1 set. This plist is the version-controlled source of truth. Edit it ONLY while iTerm2 is fully quit (see warning below).
  • Active tmux config: the XDG one at ~/.config/tmux → symlink to ~/xtmux/tmux/. NOT ~/.tmux.conf.

Do NOT edit the plist while iTerm2 is running — it gets clobbered

Despite LoadPrefsFromCustomFolder=1 and NoSyncNeverRemindPrefsChangesLostForFile=1, iTerm2 loads the custom-folder plist at launch but overwrites it with its in-memory copy on quit. The “never remind” flag only suppresses the warning dialog — it does not stop the write-back. So editing the plist while iTerm2 runs is lost on the next quit: the stale in-memory prefs win over your file edits. This was disproven in practice — the earlier claim that live PlistBuddy edits were safe is wrong.

Correct procedure (idempotent applier):

  1. Fully quit iTerm2.
  2. From a non-iTerm2 terminal (Terminal.app), run ~/.dotfiles/bin/iterm-apply-prefs — an idempotent applier that guards on pgrep -x iTerm2 and refuses to run while iTerm2 is up.
  3. Reopen iTerm2. Launch loads the edited values into memory; subsequent quits just rewrite the same values, so it’s stable from then on.

Manual edit example (only with iTerm2 quit, run from Terminal.app):

/usr/libexec/PlistBuddy -c "Set :'New Bookmarks':0:'Normal Font' 'JetBrainsMono-Regular 15'" \
  ~/.dotfiles/config/iterm2/com.googlecode.iterm2.plist

Fix 1 — iTerm2 -CC garbage dump into shell

Symptom

Raw tmux control-mode commands (refresh-client, show-window-options, …) leak into the shell as command not found.

Root cause: the profile launched Command=/bin/zsh and injected tmux -CC new -A -s main via the profile’s Initial Text field. That starts tmux -CC inside an already-interactive login shell; on detach you fall back to that outer shell and iTerm2’s queued control-mode commands get run as shell commands.

Fix: make tmux -CC the profile’s actual command, not Initial Text.

  • Custom Command = Yes
  • Command = /opt/homebrew/bin/tmux -CC new -A -s main
  • Clear Initial Text

Rule

For iTerm2 tmux -CC auto-attach, use the profile Command, never Initial Text.

Fix 2 — fatal: not a git repository printed twice on login

Symptom

fatal: not a git repository printed twice on every shell login. Also risks corrupting tmux -CC startup.

Root cause: ~/.zshrc aliases git-https / git-ssh were written with double quotes wrapping $(git remote get-url origin ...), so the command substitution ran at alias-definition time (every shell startup, including in non-repo dirs) instead of at invocation.

Fix: single-quote the alias body to defer substitution.

# bad — runs $() at definition time
alias git-https="... $(git remote get-url origin) ..."
# good — deferred to invocation
alias git-https='... $(git remote get-url origin) ...'

Rule

Alias bodies containing $(...) must be single-quoted.

Fix 3 — tmux aggressive-resize keeps reverting to on

Symptom

aggressive-resize reverts to on (breaks iTerm2 -CC sizing) despite being set off in tmux.conf / profile.tmux.

Root cause: the theme file meta.min.tmuxtheme sets aggressive-resize on, and tmux.conf re-sources that theme on every window-linked / session-created / session-renamed hook — clobbering the off on each event.

Fix: set the line in the theme file itself to off.

Rule

Anything a hook re-sources overrides one-shot config. Fix the sourced file, not the one-shot setter.

Fix 4 — Applied iTerm2 plist tweaks

All via the dotfiles iTerm2 plist:

  • Font: JetBrainsMono 13 → 15.
  • Custom Directory = Recycle — new tabs open in the current directory.
  • Key mappings for word/line navigation (these drive both the zsh line editor and Claude Code input):
    • Option+Left / Option+Right → send ESC seq b / f (backward / forward word)
    • Cmd+Left / Cmd+Right → send hex 0x1 / 0x5 (Ctrl-A / Ctrl-E = line start / end)

For Agents

Claude Code’s keybindings.json has no text-navigation actions. Claude Code honors terminal ESC-b/f and Ctrl-A/E, so terminal key-mapping config is the only lever for word/line navigation inside the Claude Code prompt. Configure it at the iTerm2 level, not in Claude Code.

Fix 5 — Second -CC window: “cannot attach tmux to this session”

Symptom

A profile that auto-runs tmux -CC new -A -s main works for the first iTerm2 window, but opening a second iTerm2 window with the same profile fails with cannot attach tmux to this session.

Root cause: iTerm2 tmux -CC allows only one control-mode client per tmux session. The first window is already attached in control-mode to session main; a second -CC client trying to attach to the same already-attached session is rejected.

Diagnose: tmux list-clients — look for a client on that session whose flags contain control-mode.

Fix (chosen resolution): split into two profiles.

  • Default profile “Main” = plain login shell — no auto-tmux, so opening multiple windows never conflicts.
  • Separate dedicated “tmux” profile runs tmux -CC new -A -s main on demand (only launch it once).

Inside the -CC window, new tabs (Cmd+T) are tmux windows, not new -CC clients — no conflict. They inherit the cwd because the tmux window/split bindings use -c "#{pane_current_path}" and the profile has Custom Directory = Recycle.

Rule

One tmux -CC control-mode client per session. Keep the default profile a plain shell; use a dedicated on-demand profile for -CC, and create additional panes/windows inside it rather than opening new -CC clients.

Note — kaku (WezTerm-based) terminal

kaku’s integration is shell-level: ~/.config/kaku/zsh/kaku.zsh is sourced from .zshrc, so its aliases/history/keybindings already work in iTerm2 regardless of terminal. Nothing terminal-specific to port — except optional tmux mouse on + shift-wheel-to-copy-mode, which the user currently keeps off deliberately.

Fix 6 — Claude Code tab name: ✦ <project>·<sid> instead of emoji number

Symptom

iTerm2 -CC native tabs took their title from the tmux window name, and the theme force-renamed every window to an emoji number — so Claude Code tabs showed a meaningless emoji instead of which project/session they held.

Goal: Claude Code tabs show ✦ <project>·<short session id> — e.g. ✦ levander·a1b2 = basename $cwd + first 4 chars of session_id. Non-CC windows keep the emoji numbering.

Why the window name is the only channel

In iTerm2 -CC the tmux status bar is hidden, so the tab title is the tmux window name — there’s no other surface to write to. Plain rename-window / -n names never stuck because the theme re-sources on window-linked / session-created / session-renamed hooks and clobbers them (same mechanism as Fix 3, and the core of the existing @cc_name finding). The durable pattern: tag the window with a custom @cc_name option and make the theme honor it, rather than fighting rename-window.

Implementation (three parts):

  1. SessionStart hook~/.dotfiles/config/claude/hooks/tmux-session-namer.sh (symlinked from ~/.claude/hooks/; the previously-dangling symlink now resolves). No-ops unless $TMUX is set. Parses session_id and cwd from the hook’s stdin JSON with sed (no jq/python dependency), computes name="✦ <basename cwd>·<first4 session_id>", then:

    tmux set-option -w -t "$TMUX_PANE" @cc_name "$name"
    tmux rename-window -t "$TMUX_PANE" "$name"

    Targeting the pane’s window via $TMUX_PANE means it works even when the window isn’t active.

  2. Registered in ~/.claude/settings.json under hooks.SessionStart → command "$HOME/.claude/hooks/tmux-session-namer.sh".

  3. Theme edit — the emoji-rename run-shell block in ~/xtmux/tmux/meta.min.tmuxtheme now reads the window’s @cc_name first: if set, it re-applies that name; otherwise it applies the emoji number as before. This is what makes the Claude Code name survive the theme’s hook re-sources. Verified: a window tagged @cc_name keeps its name through a theme re-source instead of reverting to emoji.

Rule

To make a per-window name survive the theme’s hook-driven re-renames, don’t rely on rename-window alone — tag the window with @cc_name and teach the sourced theme block to honor it. (Generalizes Fix 3’s “fix the sourced file” rule to per-window state.)

Known limitation — stale label after Claude Code exits

@cc_name persists after the Claude Code session ends, so a reused pane keeps the label until the window closes. Not yet handled; could be cleared via a SessionEnd hook if it becomes annoying.

Rollout: effective on the next Claude Code session started inside the tmux -CC window. The theme change is already live on the running server and saved to file.

Fix 7 — Claude Code Agent Teams spawned broken raw-tmux panes (nested -CC)

Symptom

With experimental Agent Teams enabled (CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1), Claude Code spawned “new panes within the tab” that were broken — each teammate window dropped into the raw tmux mode state (** tmux mode started ** / Command Menu (esc Detach, X Force-quit) / Detached) instead of a working pane.

Root cause: the teammate spawn backend was set to tmux ("teammateMode": "tmux" + env CLAUDE_CODE_SPAWN_BACKEND: "tmux"). To spawn each teammate, Claude Code ran /opt/homebrew/bin/tmux -CC new -A -s main — a second -CC control-mode client on the already-attached main session. As established in Fix 5, iTerm2 allows only one -CC control client per tmux session, so the nested -CC client couldn’t take over and the window fell back to raw tmux mode. This was not a tmux.conf or hook problem.

Fix — use iTerm2 native panes for teammate spawns

Set the teammate spawn backend to iTerm2 native panes instead of tmux, in both settings files (they have diverged and are NOT symlinked — edit each):

  • ~/.claude/settings.json (active)
  • ~/.dotfiles/config/claude/settings.json (source of truth)

In each:

"teammateMode": "iterm2",
"env": { "CLAUDE_CODE_SPAWN_BACKEND": "iterm2" }

Restart Claude Code afterward — CLAUDE_CODE_SPAWN_BACKEND is read at startup.

Requirements for iterm2 mode (both already satisfied on this machine):

  • it2 CLI installed at /Users/levander/.local/bin/it2 (Homebrew tap mkusaka/tap/it2).
  • iTerm2 Python API enabled: Settings → General → Magic → Enable Python API. Verified working via it2 window list.

Alternatives (for reference):

  • "teammateMode": "in-process" — all teammates run in the main terminal, no spawned panes. Safest.
  • "teammateMode": "tmux" — only viable if not using -CC control mode (plain tmux tolerates multiple attach clients).

Two settings files are NOT symlinked

~/.claude/settings.json and ~/.dotfiles/config/claude/settings.json have diverged and are separate files. Any Claude Code settings change must be applied to both or it won’t stick / won’t be captured in dotfiles.

Fix 8 — tmux split menu didn’t inherit cwd

Symptom

prefix > display-menu H/V Split entries opened new panes at $HOME instead of the current pane’s directory.

Root cause: the H/V split-window commands in ~/xtmux/tmux/keybindings.tmux (prefix > display-menu) were missing -c "#{pane_current_path}".

Fix: add -c "#{pane_current_path}" to both the H-Split and V-Split menu bindings. (Same cwd-inheritance mechanism as the direct split bindings noted in Fix 5.)