/split forks the current session into a terminal split beside it (cmux or tmux)

/split forks the current Claude Code session into a new terminal split beside it. The main session keeps running; the fork starts with the full conversation so far.
/split fork into a split on the right
/split down right | down | left | up
/split -n idea name the fork yourself
/split --model sonnet other claude flags pass through
<name>-branch-<n>: the session's /rename name (or its generated title), counted per name.--model / --permission-mode to override.Built on Claude Code plugin function hooks (mods); tested on 2.1.289.
hooks/register.ts 94 lines1import type { EngineInterface, Register } from 'claude-code'
2
3type Direction = 'right' | 'down' | 'left' | 'up'
4type Title = { customTitle?: string; aiTitle?: string }
5
6// Single-quotes a value for the shell the new split runs its command in.
7const quote = (value: string) => `'${value.replaceAll("'", `'\\''`)}'`
8
9// `/split [direction] [claude flags]`: the flags pass through to claude verbatim.
10const parse = (args: string): { direction: Direction; flags: string } => {
11 const trimmed = args.trim()
12 const match = /^(right|down|left|up)(?:\s+|$)/.exec(trimmed)
13 if (!match) return { direction: 'right', flags: trimmed }
14
15 return { direction: match[1] as Direction, flags: trimmed.slice(match[0].length) }
16}
17
18// The session's /rename name, else its generated title, read from its transcript;
19// mods have no title API. A fork copies these records, so -n must override them.
20const nameOf = async ($: EngineInterface, id: string) => {
21 const config = (await $.env.get('CLAUDE_CONFIG_DIR')) ?? `${await $.env.get('HOME')}/.claude`
22 const found = await $.process.run(['find', `${config}/projects`, '-maxdepth', '2', '-name', `${id}.jsonl`])
23 const transcript = found.stdout.split('\n').find(Boolean)
24 if (!transcript) return id.slice(0, 8)
25
26 const pattern = String.raw`"(customTitle|aiTitle)":"([^"\\]|\\.)*"`
27 const grep = await $.process.run(['grep', '-o', '-E', pattern, transcript])
28 const titles = grep.stdout
29 .split('\n')
30 .filter(Boolean)
31 .map(line => JSON.parse(`{${line}}`) as Title)
32
33 return (
34 titles.findLast(title => title.customTitle)?.customTitle ??
35 titles.findLast(title => title.aiTitle)?.aiTitle ??
36 id.slice(0, 8)
37 )
38}
39
40// `<name>-branch-<n>`, counted per parent name across sessions.
41const nextBranchName = async ($: EngineInterface, id: string) => {
42 const parent = await nameOf($, id)
43 const key = `branches:${parent}`
44 const n = Number((await $.store.get(key)) ?? 0) + 1
45 await $.store.set(key, n)
46
47 return `${parent}-branch-${n}`
48}
49
50export const register: Register = on => {
51 on('session.start', async ($, e, next) => {
52 await $.command.register({
53 name: 'split',
54 description: 'Fork this session into a split beside it, named <name>-branch-<n>',
55 argumentHint: '[right|down|left|up] [claude flags]',
56 immediate: true,
57 })
58
59 return next(e)
60 })
61
62 on('command.run', { command: 'split' }, async ($, e) => {
63 const { direction, flags } = parse(e.args)
64 const [id, cwd] = await Promise.all([$.session.id(), $.session.cwd()])
65 const hasName = /(?:^|\s)(?:-n|--name)(?:[\s=]|$)/.test(flags)
66 const name = hasName ? undefined : await nextBranchName($, id)
67 const claude = ['claude', '--resume', quote(id), '--fork-session', name && `--name ${quote(name)}`, flags]
68 .filter(Boolean)
69 .join(' ')
70 const shell = `cd ${quote(cwd)} && ${claude}`
71 const done = { text: `Branched into a ${direction} split${name ? ` as ${name}` : ''}.` }
72
73 // cmux splits the surface named by CMUX_SURFACE_ID, the one this session runs in.
74 if (await $.env.get('CMUX_SURFACE_ID')) {
75 const cmux = (await $.env.get('CMUX_BUNDLED_CLI_PATH')) ?? 'cmux'
76 const run = await $.process.run([cmux, 'new-split', direction, '--command', shell, '--focus', 'true'])
77 if (run.exitCode === 0) return done
78
79 return { text: `cmux new-split failed: ${(run.stderr || run.stdout).trim()}\nRun it yourself: ${shell}` }
80 }
81
82 if (await $.env.get('TMUX')) {
83 const axis = direction === 'right' || direction === 'left' ? '-h' : '-v'
84 const before = direction === 'left' || direction === 'up' ? ['-b'] : []
85 const run = await $.process.run(['tmux', 'split-window', axis, ...before, '-c', cwd, shell])
86 if (run.exitCode === 0) return done
87
88 return { text: `tmux split-window failed: ${(run.stderr || run.stdout).trim()}\nRun it yourself: ${shell}` }
89 }
90
91 return { text: `Not in cmux or tmux. Run this in a new terminal:\n${shell}` }
92 })
93}
94