From d23778942d48ba13179e961c68377b2185904b8c Mon Sep 17 00:00:00 2001 From: Paul O'Reilly Date: Mon, 26 Oct 2026 00:26:51 +1300 Subject: [PATCH] claude-sessions spawn: add --headless/--create for non-interactive use Claude-Session: https://claude.ai/code/session_01JXneLTuvb5Gxim3DTN2tQa --- memory/script-claude-sessions.md | 4 ++++ memory/skill-spawn-session.md | 8 +++---- scripts/claude-sessions | 38 ++++++++++++++++++++++++++++---- 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/memory/script-claude-sessions.md b/memory/script-claude-sessions.md index a248efb..6f18d3c 100644 --- a/memory/script-claude-sessions.md +++ b/memory/script-claude-sessions.md @@ -16,6 +16,10 @@ Python script (`scripts/claude-sessions`, symlinked to `~/sbin/`). Manages Claud - **Folder browser.** `zenity --file-selection --directory` when `DISPLAY`/`WAYLAND_DISPLAY` is set and zenity exists (has Create Folder). Otherwise a terminal browser: number = open, `u` up, `n` new folder, `/text` filter, `>` `<` page, `h` hidden, `~`/`/path` jump, Enter/`s` select, `q` cancel. - **Topic.** Becomes the tmux session name and the Remote Control name (passed as `--name` to `claude-tmux`). Defaults to the Title-Cased folder name. If that name is already a live tmux session, no default is offered and a unique topic is required. Topics containing `.` or `:` are rejected because tmux rewrites them to `_`. +## Headless spawn + +`claude-sessions spawn --headless [--name topic] [--create] [--mode m] [--profile p]` — no prompts, no attach, for scripts and the `/spawn-session` skill. Exit 1 = folder missing (or `--create` needs an absolute path); exit 2 = topic in use (default topic is the Title-Cased folder name; no `#N` suffix is added). Without `--headless`, `spawn` behaves as before. + ## Gotchas - "In use" means a live tmux session. Resumable registry entries do not block a topic. diff --git a/memory/skill-spawn-session.md b/memory/skill-spawn-session.md index af68087..ed8757e 100644 --- a/memory/skill-spawn-session.md +++ b/memory/skill-spawn-session.md @@ -12,14 +12,14 @@ from the Claude app. - `/spawn-session ` — spawn for a named project folder - `/spawn-session --mode ` — override engagement mode -- `/spawn-session --attach` — attach after spawning - `/spawn-session` — lists projects and asks which one ## How it works -Thin wrapper over the `claude-tmux` script (in `small-scripts`, symlinked to `~/sbin`). -The skill only resolves the project name from `$ARGUMENTS` (or asks) and shells out to -`claude-tmux [flags]`. All real logic — folder resolution, mode selection +Calls `claude-sessions spawn --headless [--name topic] [--create]` (since 2026-10-26), +which wraps `claude-tmux` (in `small-scripts`, symlinked to `~/sbin`). The skill only resolves +the folder from `$ARGUMENTS` (or asks). Headless mode never prompts or attaches, and exits 2 +if the topic is already a live tmux session (the skill then asks for a unique topic). All real logic — folder resolution, mode selection (profile `last-mode` unless `--mode`), Title-Casing, ` #N` auto-increment, tmux creation, and launching `claude --remote-control` via `claude-profile` — lives in the script. diff --git a/scripts/claude-sessions b/scripts/claude-sessions index 6ad7f76..9471482 100755 --- a/scripts/claude-sessions +++ b/scripts/claude-sessions @@ -6,7 +6,7 @@ Commands: (no args) Interactive menu (new session: folder browser + unique topic) list Show live Claude tmux sessions and resumable registry entries recover [--days N] Detect sessions active before a restart - spawn Spawn a new session (wraps claude-tmux) + spawn Spawn a new session (wraps claude-tmux); --headless for scripted use resume Resume a past session by conversation ID register Record a new session in the registry (called by claude-tmux) gc [--days N] Prune stale entries and backfill session IDs @@ -650,13 +650,39 @@ def cmd_spawn(args) -> None: claude_tmux = subprocess.run(["which", "claude-tmux"], capture_output=True, text=True).stdout.strip() if not claude_tmux: die("claude-tmux not found in PATH") - cmd = [claude_tmux, args.project] + project = args.project + name = args.name + + if getattr(args, "headless", False): + # Non-interactive: no prompts, no attach. Errors exit non-zero instead of asking. + folder = resolve_project_dir(project) + if folder is None and args.create: + folder = Path(project).expanduser() + if not folder.is_absolute(): + die(f"--create needs an absolute path (got {project!r})") + try: + folder.mkdir(parents=True) + except OSError as e: + die(f"Could not create {folder}: {e}") + folder = folder.resolve() + if folder is None: + die(f"Folder not found: {project} (use --create with an absolute path to make it)") + name = name or title_case(folder.name) + if topic_in_use(name, list_tmux_sessions()): + print(f"{RED}Error:{NC} topic '{name}' is already in use — pass a unique --name", file=sys.stderr) + sys.exit(2) + if any(c in name for c in ".:"): + die("Topic must not contain '.' or ':' (tmux rewrites them)") + project = str(folder) + args.attach = False + + cmd = [claude_tmux, project] if args.mode: cmd += ["--mode", args.mode] if args.profile: cmd += ["--profile", args.profile] - if args.name: - cmd += ["--name", args.name] + if name: + cmd += ["--name", name] if args.attach: cmd += ["--attach"] os.execvp(claude_tmux, cmd) @@ -919,6 +945,10 @@ def main() -> None: sp.add_argument("--profile", default=None) sp.add_argument("--name", default=None) sp.add_argument("--attach", action="store_true") + sp.add_argument("--headless", action="store_true", + help="Non-interactive: never prompt or attach; fail (exit 2) if the topic is in use") + sp.add_argument("--create", action="store_true", + help="With --headless: create the folder (absolute path) if it does not exist") rsp = sub.add_parser("resume", help="Resume a past conversation by session ID") rsp.add_argument("session_id", help="Session UUID or prefix")