feat: fork a Claude Code session into a throwaway popup - #29
Open
ytaskiran wants to merge 13 commits into
Open
Conversation
Add find_pane_agent() and agent_in_pane() to look up which agent, if any, runs in a specific pane. Unlike scan_agents(), agent_in_pane() does not filter by tws-session membership, allowing fork detection to work in any pane. - find_pane_agent(): private pure function for testability - agent_in_pane(): public IO wrapper that queries tmux and processes Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FrL7CS8ub31uQ4MmUw7aJf
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FrL7CS8ub31uQ4MmUw7aJf
The existing cwd-mismatch test did not verify order because transcript_exists was always true. Add a test with mismatched pane_cwd and transcript_exists=false to ensure cwd check runs before transcript check. Proves order is load-bearing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FrL7CS8ub31uQ4MmUw7aJf
The command reads the pane pointer file. It checks the facts. It calls claude with --resume and --fork-session. The command sets TWS_FORK=1. This stops the new session from overwriting the parent pointer.
Add SessionStart and SessionEnd hooks that write a pointer file at config/tws/sessions/<pane_id>. The pointer holds the session id and cwd, so tws fork-pane can fork the session. Widen the is_tws marker test to match both config/tws/agents and config/tws/sessions, so re-runs stay idempotent.
The tmux binding cannot pass a pane id. tmux does not expand
#{pane_id} inside a display-popup shell-command.
fork-pane now asks tmux for the current pane id when the caller
omits it. The manual form with an explicit pane id still works.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FrL7CS8ub31uQ4MmUw7aJf
Add configure_fork_binding to install.sh. The step asks the user before it writes a tmux key binding. It marks the feature as EXPERIMENTAL in the prompt and the success message. The step skips when ~/.tmux.conf does not exist. The step warns and does not overwrite an existing binding on prefix+F. The step is idempotent across repeat runs. Add a README section for the session fork feature. The section marks the feature as EXPERIMENTAL and lists Claude Code as the only supported agent.
The old check read only the live tmux server. A user could add bind-key F to ~/.tmux.conf by hand and not source it yet. The old check missed this case and let the installer add a second F binding. The check now also greps the config text for a bind-key F line. It skips the block that the tws marker owns, so a re-run still finds only its own line and stays idempotent. A commented-out line does not count as a conflict. Rewrite two README passages to follow the Simplified Technical English rules: short sentences, active voice, one idea per sentence.
One sentence in the session-fork section chained two instructions and ended in the passive voice. Split it into three short active sentences, one instruction each.
find_pane_agent and agent_in_pane are live code now. agent_in_pane is called from fork.rs. The allow(dead_code) attributes hid this fact from clippy and readers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A tmux popup reports the parent pane as $TMUX_PANE, not its own pane. Every tws hook resolves its pane with that value, so a forked session's own hooks were acting on the parent pane. - Add a TWS_FORK guard to session_end_entry, and to the two agent status cleanup entries (Claude and Codex SessionEnd). Without the guard, a fork's exit deleted the parent's session pointer and agent status marker. This made prefix+F work only once per Claude session. - Add the same guard to status_hook_entry, so a fork's own working/waiting/review hooks no longer overwrite the parent pane's status in the tws agents view. - Widen the tmux key-conflict check to accept the bind alias (not only bind-key) and any leading flags (-r, -T <table>). A bind with -n or -T root targets the root table, not the prefix table, so it is excluded from the conflict check. - Make the strip step in configure_fork_binding resilient to grep finding nothing to remove, so it no longer aborts the installer under set -o pipefail. - Skip the prefix+F binding offer, with an explanation, when the Claude Code hooks were never configured. The binding is useless without the SessionStart hook. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Merge upstream status hook changes into the session fork branch. Keep the TWS_FORK guard on every hook command. Add the guard to upstream's new hand-written SessionEnd commands. Widen is_tws in both hook functions to match config/tws/sessions. Remove CLAUDE_HOOKS_CONFIGURED and read upstream's hooks_configured flag instead.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this adds
Press
prefix+Fin a Claude Code pane. tmux opens a popup. The popup runs a forked copy of that pane's Claude session.The fork inherits the whole conversation. It writes to a new session id. The parent transcript stays untouched. Ask a throwaway question. Close the popup. The fork leaves nothing behind.
This solves a context problem. A side question in the main thread costs context that the real task needs. A new session loses the context that makes the question answerable.
How it works
SessionStarthook writes<session_id>\t<cwd>to~/.config/tws/sessions/<pane_id>.tws fork-pane.tws fork-paneresolves its own pane, reads the pointer, validates it, then runsclaude --resume <id> --fork-session.TWS_FORK=1makes every hook inside the fork exit early. A fork must not touch its parent's files.install.shinstalls the hooks. It also offers the tmux binding as an opt-in, experimental step.Facts we measured
We tested these before we designed the feature. They constrain the design.
--fork-sessionleaves the parent transcript untouchedclaudeprocess shows no session id inps. The transcript file stays closed, solsoffinds no holder.#{pane_id}in adisplay-popupcommandtws fork-panetherefore resolves its own pane.$TMUX_PANEis emptytmux display-message -p '#{pane_id}'returns the originating pane instead. Every hook inside a fork therefore acts on the parent pane, unless a guard stops it.codex resumeappends to the parent rolloutScope
Claude Code only. A Codex or Pi pane reports that forking is unsupported and launches nothing.
Error handling
Every failure prints in the popup and waits for a key. Nothing launches and nothing changes on disk.
fork: no agent running in this panefork: only Claude Code supports forking — codex resume appends to the parent sessionfork: no session recorded for this pane ...fork: the recorded session no longer existsfork: the recorded session belongs to a different directoryThe transcript check fails open. The path rule is an undocumented Claude Code internal, so a change upstream must not break the feature.
Tests
165 tests pass. The four CI jobs pass:
fmt,clippy -D warnings,test, andaudit.classifyis a pure function over aFactsstruct, so every precondition is testable without tmux, without a filesystem, and without Claude. One test is load-bearing: it fails if somebody moves the directory check after the transcript check.We also verified the feature against real tmux panes:
tws fork-paneresolves the correct pane.--resume <sid> --fork-session, in the recorded directory, withTWS_FORK=1.install.shstays idempotent across three runs. The hook counts do not change and the file stays byte-identical.bind Fandbind-key -r F. It ignores a commented line.Known limits
TWS_FORKguard.bind-key -N "note" F, because a multi-word-Nargument breaks the flag grammar. This is a follow-up.bind -T <custom-table> Fcounts as a conflict, although it binds another table. We chose caution over a missed conflict.Upgrade note
The hook marker widens from
config/tws/agentstoconfig/tws/(agents|sessions). Existing installs carry the old marker only, so the first re-run strips both families and adds both back. Re-runs stay idempotent.