feat(claude-tmux): spawn detached tmux Claude Code session with Remote Control

New script that launches Claude Code (via claude-profile) inside a detached
tmux session with --remote-control enabled, in a project's directory, named
after the project (Title-cased, auto-incrementing #N on collision). Lets a
session be started for later attach over VPN or control from the Claude app.

Spec-first per repo conventions: specs/claude-tmux.spec.md, script, and
tests/test-claude-tmux.sh (18 dryrun assertions, all passing).

Claude-Session: https://claude.ai/code/session_014oa7T7z1h5n34vu8vX9zGT
This commit is contained in:
Paul O'Reilly
2026-07-25 16:29:46 +12:00
parent 0476c00e81
commit 448091ecf4
4 changed files with 375 additions and 0 deletions

105
specs/claude-tmux.spec.md Normal file
View File

@@ -0,0 +1,105 @@
# claude-tmux
## Purpose
Spawn a detached `tmux` session running Claude Code (via `claude-profile`) with
Remote Control enabled, in a project's directory, with the session named after
the project so it is easy to `tmux attach` to and easy to identify in the Claude
app.
## Usage
```
claude-tmux [PROJECT] [OPTIONS]
```
### Arguments
| Argument | Default | Description |
|----------|---------|-------------|
| `PROJECT` | *(prompt if TTY)* | Project name (folder under `~/dev/claude` or `~/dev/claude/projects`), or an explicit path to a directory |
### Flags
| Flag | Default | Description |
|------|---------|-------------|
| `--mode <name>` | profile's `last-mode`, else `quick` | Engagement mode passed to `claude-profile` |
| `--profile <name>` | `oreillyit-anthropic` | Claude Code profile (resolves to `~/.claude-<name>`, or `~/.claude` for `default`) |
| `--name <display>` | Title-cased project name | Override the session / Remote-Control name |
| `--attach` | off | Attach (or `switch-client` if already inside tmux) after spawning |
| `--dryrun`, `-n` | off | Preview all actions without creating anything |
| `--help`, `-h` | | Show usage information |
## Behaviour
1. Resolve the profile directory (`~/.claude-<profile>`, or `~/.claude` for
`default`). Error if it does not exist.
2. Resolve the project. If `PROJECT` is empty and stdin is a TTY, prompt for it;
if empty and not a TTY, error. Resolve the folder by trying, in order: an
existing path, `~/dev/claude/<PROJECT>`, `~/dev/claude/projects/<PROJECT>`.
Error if none exist.
3. Resolve the engagement mode. If `--mode` is not given, read the profile's
`last-mode` file, falling back to `quick`. The mode is **always** resolved
explicitly and passed to `claude-profile` — the launcher must never leave
`claude-profile` to show its interactive mode picker, because Remote Control
only registers in the app once `claude` is actually running.
4. Derive the base session name: Title-Case the project folder basename
(`agent-runtimes``Agent Runtimes`), unless `--name` overrides it.
5. Auto-increment: if a tmux session with the base name already exists, append
` #2`, ` #3`, … until a free name is found. The first session gets no suffix.
6. Create a detached tmux session named after the resolved name, with its working
directory set to the project folder.
7. Send the launch command into the session's pane (sent literally, followed by
Enter):
`claude-profile <profile> --mode <mode> --project <basename> -- --remote-control "<name>"`
8. Print the attach hint and the Remote-Control name. If `--attach` is set,
attach (or `switch-client` when already inside tmux).
`claude-profile` is located via `PATH`, falling back to a sibling of this script.
## Dryrun Behaviour
When `--dryrun` / `-n` is passed, no tmux session is created and nothing is sent.
Output (values substituted):
```
[dryrun] Project: agent-runtimes (/home/paul/dev/claude/projects/agent-runtimes)
[dryrun] Profile: oreillyit-anthropic (/home/paul/.claude-oreillyit-anthropic)
[dryrun] Mode: deep
[dryrun] Session name: Agent Runtimes
[dryrun] Would run: tmux new-session -d -s "Agent Runtimes" -c "/home/paul/dev/claude/projects/agent-runtimes"
[dryrun] Would send: claude-profile oreillyit-anthropic --mode deep --project agent-runtimes -- --remote-control Agent\ Runtimes
[dryrun] Attach with: tmux attach -t "Agent Runtimes"
```
## Edge Cases
| Case | Handling |
|------|----------|
| No PROJECT and stdin is a TTY | Prompt `Project name:` |
| No PROJECT and not a TTY (e.g. run by an agent) | Error: "No project given…", exit 1 |
| Project folder not found | Error naming the searched locations, exit 1 |
| Profile directory not found | Error, exit 1 |
| Session name already taken | Auto-increment with ` #N` suffix |
| Project name has underscores | Treated like hyphens for Title-Casing |
| `claude-profile` not found | Error, exit 1 |
| Two concurrent sessions on the same profile | Both work: `claude-profile` rewrites the shared `active-mode.env`, but each session's **cwd takes precedence** for project auto-selection, so the correct project is still picked |
## Examples
```bash
# Spawn a Remote-Control session for agent-runtimes (Title-cased, mode from last-mode)
$ claude-tmux agent-runtimes
Spawned tmux session: Agent Runtimes
attach: tmux attach -t "Agent Runtimes"
# A second one auto-increments
$ claude-tmux agent-runtimes
Spawned tmux session: Agent Runtimes #2
# Override the mode, and attach immediately
$ claude-tmux cluster-bootstrap --mode deep --attach
# Preview only
$ claude-tmux dns-manager --dryrun
```