The sessionhub client commands read from the sessionhub server and get you back into a
session. They use the client config (~/.config/sessionhub/config.toml, env
SESSIONHUB_CONFIG, SESSIONHUB_SERVER_URL, SESSIONHUB_TOKEN, SESSIONHUB_MACHINE) and the 2-second
HTTP timeout from docs/client.md.
| Command | What it does |
|---|---|
sessionhub ls [--all] [--machine M] [--grep TEXT] |
Table of machine, short ID, title, status, age (since last seen), and the latest report's summary. Without --all it lists live and blocked sessions only. --grep TEXT keeps the sessions whose title, recap, last prompt, branch, directory, or machine contains TEXT. The match is plain text and ignores case, so [build] is not a pattern. To search ended sessions, combine --grep with --all. If no live session matches but ended or stale ones do, the command says how many and suggests --all. |
sessionhub show <id-prefix> |
One session in full: fields (including recap:, last prompt:, and a one-line summary: of commits, latest merge request, and cost), the session digest, report history, and the last 50 events, newest first. The digest lists up to five commits made during the session, the links the session touched, token totals (input, output, cache read, cache write), the cost with its as of time, and when the digest was taken. |
sessionhub status |
Server URL, machine name, server health, and this machine's non-ended sessions. The sessionhub.status plugin action runs it. Exits non-zero when the server is unreachable. |
sessionhub login --name <name> [--no-qr] [--json] |
Create a one-time link that signs a browser in to the dashboard as <name>, and print the link, when it expires, and a QR code of it. --no-qr omits the QR code. --json prints only {"url":...,"name":...,"expires_at":...}. See below. |
sessionhub login ls [--json] |
Signed-in browsers: name, ID, machine, created, last used, and expiry. |
sessionhub login rm <name|id> |
Sign a browser out. It matches a name first, then an ID. No confirmation. |
sessionhub inbox [--json] |
The sessions that need you, across machines, under the headings Blocked, Waiting on you, and Finished, oldest first in each. One line per item: ID prefix, title, machine, how long ago it arrived, stale for a stale session, and what it waits on (for a blocked session with a permission request, asks to use <tool>: <command or file>) or its recap, cut to the terminal width. --json prints the server response. See below. |
sessionhub inbox --watch |
The same list, full screen, refreshed every 5 seconds, with a cursor you move with the keyboard. See below. |
sessionhub inbox dismiss [--since <time>] <id|prefix> |
Hide an inbox item until something new happens in that session. |
sessionhub inbox snooze <id|prefix> <1h|4h|tomorrow|duration> |
Hide an inbox item for a while. tomorrow means 9:00 local time the next day. A Go duration such as 90m also works, up to just under 7 days (167h59m). |
sessionhub knowledge [ls] [--state proposed|active|all] [--scope S] |
Knowledge entries, one per line: number, kind, scope, text, state, and who added it. Default --state active. See below. |
sessionhub knowledge add --kind K --scope S [--why W] [--every N] "<text>" |
Propose an entry. It reaches sessions after you approve it on the dashboard. --every N takes 0 (every prompt) to 100; without it, a fact or decision defaults to 20 and a convention to 0. |
sessionhub knowledge search "<query>" [--scope S] |
Search the active entries, at most 20. |
sessionhub rules [ls] |
The global conventions every session sees, one per line: number, text, and who added it. See below. |
sessionhub rules add [--every N] "<text>" |
Add an active global convention, 1 to 300 characters on one line. The conventions hold at most 2,000 characters. --every N sends it with every Nth prompt of a session instead of every prompt. |
sessionhub rules rm <id> |
Retire convention <id>. |
sessionhub task add "<title>" [--ref X] [--url U] [--source S] [--now] |
Add a task. It starts as todo, or in_progress with --now. --source is ticket, email, chat, or other (default other). See below. |
sessionhub task ls [--date YYYY-MM-DD] [--json] |
A day (default today) in the local time zone, in sections: Now, Waiting on, Done, Touched, not finished, Todo, and Carried over, each with its working time. Empty sections are left out. --json prints the day view as is, with active_seconds, last_active_at, now, and waiting_on. |
sessionhub note <text> |
Send a worklog note. The session is CLAUDE_CODE_SESSION_ID when set. sessionhub routes it to a task: a ticket ref (systemsdev-NNNNN, !NNNN) picks that task, else the session's current task, else a new one; done, finished, or (with a ref) close/closed at the start marks it done. Prints noted → <ref> <title> (<action>). If the server can't be reached, the note is queued and sent later (queued: <text>). worklog note calls it. |
sessionhub task review |
The review queue: Proposed, Awaiting done, and Sessions with no task. |
sessionhub task accept|reject|done|drop|start|reopen <id> [--note T] |
Change a task's state. See below. |
sessionhub task merge <id> --into <id> |
Merge a task into another; the first is dropped. |
sessionhub send <id-or-prefix>... -m "<text>" |
Send a message to sessions. See below. |
sessionhub send --machine <M> -m "<text>" |
Send a message to every live session on <M> that can take one. |
sessionhub approve [--yes] <request-id|prefix> |
Allow a pending permission prompt once, after it shows the request and asks. See below. |
sessionhub deny [--yes] <request-id|prefix> ["reason"] |
Deny a pending permission prompt, with an optional reason Claude sees. |
sessionhub move <id-or-prefix> <machine|cloud> |
Move a session to another machine, or hand it to a Claude Code cloud session, and print its progress for up to 3 minutes. See below. |
sessionhub move --status <move-id> |
Print where a move is. |
sessionhub move-key |
Print this machine's move key fingerprint (16 hex characters), creating the key if there is none. |
sessionhub start <machine> [--dir D] [--trust] [-m "<prompt>"] |
Start a new Claude session on <machine>, with Remote Control on, in a new herdr workspace, and print its link. See below. |
sessionhub resume [--force] [--remote-control] <id-prefix> |
Get back into a session. See below. --force starts a session the server reports live or blocked even though no pane runs it to focus. --remote-control starts Claude with Remote Control on. Flags go before the prefix. |
sessionhub resume --pick [--force] [--remote-control] |
Numbered list of this machine's sessions (live first, then newest; at most 30). Type a number, then the same logic as sessionhub resume. Empty input cancels. |
sessionhub remote-control <id-prefix> |
Turn on Claude Code Remote Control for a session. See below. -h or --help prints the usage. |
sessionhub install-mod [--settings FILE] [--binary PATH] |
Install the sessionhub Claude Code mod. See below. |
sessionhub uninstall-mod [--settings FILE] |
Remove the sessionhub Claude Code mod. |
sessionhub mod <command> |
The calls the sessionhub Claude Code mod makes. Not meant to be run by hand. See below. |
sessionhub plugin open-picker |
Runs herdr plugin pane open --plugin sessionhub --entrypoint resume-picker --placement overlay --focus, which runs sessionhub resume --pick. It uses HERDR_BIN_PATH when set. |
sessionhub plugin open-inbox |
Runs herdr plugin pane open --plugin sessionhub --entrypoint inbox --placement split --direction right --focus, which runs sessionhub inbox --watch in a pane to the right. It uses HERDR_BIN_PATH when set. The pane opens in the herdr of the machine the command runs on, so run it in the herdr you are looking at; outside a herdr pane (for example over SSH), it prints a note saying where the pane opened. |
A prefix needs at least 4 characters. If it matches more than one session, the server answers 409 and the error lists the candidate IDs.
The report summary shows the first in-flight item (doing:), else the first
waiting-on item (waiting:), else the last done item (done:), else the note.
If the session has no report, or its latest report is older than its latest
note (sessionhub note), sessionhub show prints that note under the
reports, for example:
In flight: systemsdev-1 fix X · note 14:05
start fix X systemsdev-1
The label is In flight: while the note's task is open, Done: when it is
done, and Note: when it is dropped. The time is local. The second line is
the note text, left out when it equals the task title. See last_note in
server.md.
Every string from the server or herdr passes through one helper
(internal/cli/termtext) before it reaches the terminal: control characters
(including escape sequences' ESC) and Unicode bidirectional controls are
removed, and newlines and tabs become spaces. sessionhub ls and the --pick list
show at most 60 characters of a title, and sessionhub ls at most 60 of the report
summary. Printed shell commands are the exception: they keep every byte and
rely on quoting instead.
sessionhub resume fetches the session, then compares its machine with machine
in the client config.
| Session | herdr state | What happens |
|---|---|---|
| This machine | herdr_pane exists, herdr detects Claude in it, and its agent_session matches |
pane.focus on it. |
| This machine | herdr_pane exists and its agent_session matches, but herdr detects no agent (the pane is at a shell prompt, as a restored pane is) |
agent.start in that pane with kind: "claude", args: ["--resume", <uuid>], name sessionhub-<first 8>, then pane.focus on it. |
| This machine | The pane is gone, runs another session, or runs another agent | pane.split (right, focused, session's cwd, target HERDR_PANE_ID else the snapshot's focused pane), then agent.start with kind: "claude", args: ["--resume", <uuid>], name sessionhub-<first 8>. |
| This machine | herdr is not running | Prints cd <cwd> && claude --resume <uuid>. |
| This machine | The session has no herdr info | Same printed command; herdr is not contacted. |
| Another machine | Has herdr info | Prints ssh -t <ssh_host> '~/.local/bin/sessionhub resume <uuid>' and herdr --remote <herdr_host>. |
| Another machine | Has a pane, and a saved herdr machine matches | Also prints herdr --machine <label> pane focus <pane_id>. |
| Another machine | No herdr info | Prints ssh -t <ssh_host> "exec \$SHELL -lc 'cd <cwd> && claude --resume <uuid>'". |
The remote branch only prints. sessionhub never runs ssh or a remote herdr.
A command that ssh runs gets a non-interactive shell whose PATH lacks
~/.local/bin, where both sessionhub and claude are installed. So the printed
sessionhub resume calls ~/.local/bin/sessionhub by path, and the no-herdr form runs
claude through the remote login shell ($SHELL -lc), which reads the profile that
sets PATH. The remote command is quoted for your local shell, so the ~ and
$SHELL reach the remote shell unexpanded and it expands them to the remote
home and shell. The server's resume_command field uses the same
ssh -t <ssh_host> '~/.local/bin/sessionhub resume <uuid>' form.
Pane IDs belong to one herdr server. If the session's herdr_session differs
from the current server's session name, sessionhub resume skips the focus step and
starts a new pane.
If the split succeeds but agent.start fails, the new empty pane stays, and
the error prints the shell command to run instead.
If the server reports the session live or blocked but no pane runs it to
focus, sessionhub resume doesn't start it: the session may still run somewhere
else, such as another herdr server or a plain terminal, and a second
claude --resume of the same session would write to the same transcript. It
prints a warning with the sessionhub resume --force <uuid> command, changes
nothing in herdr, and exits non-zero. In --pick, the warning stays on screen
until you press Enter. When sessionhub resume only prints a command, it prints the
same warning above it.
If the session has an open move (requested, packing, uploaded, or
unpacking), sessionhub resume refuses it, even with --force, on every machine:
until the move ends, the source or the target may start the session, so a
resume would run it in two places. It prints the
sessionhub move --status <move-id> command, changes nothing in herdr, and exits
non-zero.
In --pick, when the result is a printed command rather than a herdr action,
the picker waits for Enter so the overlay does not close over the text.
The flag adds --remote-control (with no name) to every way sessionhub resume
starts Claude: the agent.start args become
["--resume", <uuid>, "--remote-control"], and every printed claude command
ends with --remote-control. The printed remote command passes the flag on:
ssh -t <ssh_host> '~/.local/bin/sessionhub resume --remote-control <uuid>'. If a
pane already runs the session, sessionhub resume only focuses it, because a running
Claude can't take a flag; it prints sessionhub remote-control <uuid> to use instead.
sessionhub remote-control <id-prefix> fetches the session (same lookup and ID
validation as sessionhub resume) and compares its machine with machine in the
client config.
| Session | State | What happens |
|---|---|---|
| This machine | The pane exists, herdr detects Claude in it, and its agent_session matches |
Reads the pane (pane.read, recent_unwrapped, 120 lines), sends /remote-control with agent.prompt (target is the pane ID), then reads the pane every 500 ms for up to 10 s. Prints the last https://claude.ai/code/session_... URL that was not in the first read (waits up to 10 s; Claude runs the command at once, even mid-turn). If none appears, prints that the command was sent and to check the pane or the Claude app, and exits 0. |
| This machine | herdr answers agent_blocked (Claude waits on a permission or other prompt) |
Exits 1 with "the session is waiting on a prompt in its pane; answer it, then run this again". herdr sends no input and sessionhub doesn't retry. |
| This machine | Ended, no pane recorded, pane gone, pane runs no Claude or another session, the session belongs to another herdr server, or herdr is not running | Exits 1 with "session is not running; start it with: sessionhub resume --remote-control <id>". |
| Another machine | Any | Posts a control request through the sessionhub (POST /v1/sessions/{id}/remote-control, with this machine's token), prints Asked machine "<name>" to turn on Remote Control for session <short ID>. Waiting for it..., then reads the session every 2 s for up to 2 minutes. That machine's watcher runs the request as in docs/plugin.md, "Control loop", so an ended session there is resumed in a new herdr workspace. Prints the link (exit 0), says that no link appeared (exit 0), or exits 1 with the machine's reason. If the sessionhub refuses the request (for example 409: not in herdr, or the machine's watcher is offline), can't be reached, or the request expires, prints why, then ssh -t <ssh_host> '~/.local/bin/sessionhub remote-control <uuid>', and herdr --machine <label> agent prompt <pane_id> /remote-control when the session has a pane and a saved herdr machine matches. Never runs either, and exits 0. |
herdr decides whether the session runs, not the server's status, so a session
the server still lists as ended works when its pane runs it. From another
machine, sessionhub prints a link from the sessionhub only when the whole value matches
the pattern below, and prints the machine's detail in parentheses after it,
for example (Remote Control was already on). Only the URL
that matches https://claude.ai/code/session_[A-Za-z0-9_-]+ is printed, after
the pane text passes through the terminal-text helper. A URL that was already
on screen before the command doesn't count.
If Remote Control is already on, Claude answers /remote-control with a
dialog (Disconnect this session, Show QR code, Continue) that shows
the session URL instead of printing a new line. sessionhub judges the dialog on the
currently visible screen only (pane.read with source visible), never on
scrollback, because ordinary output such as a grep of the fixtures can hold
every string of it. All of these must hold within the last 12 non-blank visible
lines: "Disconnect this session", then "Show QR code", a URL that matches the
pattern, and the footer "Esc to continue" as the last line. When they do, sessionhub
prints "Remote Control is already on for session ..." and the URL, then closes
the dialog with pane.send_keys esc (Continue is the default). It doesn't
wait out the 10 s and exits 0.
Just before it sends esc, sessionhub reads the pane again. It sends nothing if
Claude's agent_status is working (Esc during a turn interrupts it) or if the
dialog is no longer on the visible screen. It says so, and asks you to press
Esc in the pane, if the check or the esc fails. If the dialog is already
on the visible screen before sessionhub sends anything, it exits 1 without sending,
because the command would go into the dialog.
sessionhub doesn't check agent_status first. Claude runs /remote-control at once
even while it works on a turn, and the turn continues (see
docs/dev/evidence/remote-control.md).
Text from the pane is cleaned before URL matching: control characters become
spaces, so they can't join two IDs. sessionhub refuses a recorded pane ID that
isn't herdr-shaped (^[A-Za-z0-9][A-Za-z0-9:._-]{0,63}$, for example a leading
hyphen) before it prints or uses it.
The herdr --machine ... agent prompt form sends the command without the
checks above. If the agent is blocked, herdr rejects it the same way.
herdr machine list --json runs with a 2-second timeout. Any failure, or
output the parser cannot read, counts as "no saved machines". A profile matches
when its SSH target host (without user@, scheme, or port) equals the
machine's herdr_host, case-insensitively. Disabled profiles are skipped.
The JSON field names (label, ssh_target, enabled) are not verified
against a real profile: no machine was saved when this was written. See
testdata/README.md.
sessionhub resume and sessionhub remote-control refuse a session ID that is not letters, digits, and hyphens (or starts with a hyphen), and refuses to print a remote recipe when the SSH or herdr host is empty or starts with a hyphen. Every value in a printed command (ID, hosts, pane, label, cwd) is single-quoted when it has unsafe characters, and the no-herdr remote command is quoted three times deep: once for your local shell, once for the remote shell, and once for the remote login shell. Each shell parses its layer back to the intended arguments; the tests check this with sh for every hostile value. The local layer uses double quotes with $ escaped when that is safe, and single quotes when the command holds a ", `, \, !, or newline. Comments in the output never include session data. The remote-control forms get the same hostile-value tests, and --remote-control is one plain word in every layer.
sessionhub login signs a browser in to the dashboard without copying a secret. Run
it on any machine with a client config; it uses that machine's token.
sessionhub login --name phoneOpen the printed link on the device, or scan the QR code, and press Sign in. The link works once, for 10 minutes. The browser stays signed in for 30 days after its last visit.
- The name follows the machine-name rules: 1 to 64 letters, digits,
.,_, or-, starting with a letter or digit. If a browser session already has the name, the command fails and suggestssessionhub login rm <name>. - At most 5 unused links exist at a time; the server answers
429above that. - Nothing is queued. If the server is unreachable, the command fails.
- The QR code uses half blocks, two module rows per line, with light modules
drawn filled so the code reads dark on light in a dark terminal. If your
terminal is light and your phone doesn't scan it, use
--no-qrand open the link another way. - Sign out on the dashboard ends only that browser's session.
sessionhub login rmsigns out any browser.
sessionhub inbox lists the sessions that need you, the same list as the
dashboard's Inbox tab:
- Blocked: Claude waits on a prompt in its pane.
- Waiting on you: the latest report lists
waiting_onitems, and you haven't sent a prompt since. - Finished: Claude finished a turn, and you haven't sent a prompt since.
A session appears once, in the first group that matches. Ended sessions
leave the inbox; stale ones stay, marked stale. The line width is the
terminal's, else COLUMNS, else 100 characters. An empty inbox prints
nothing needs you.
sessionhub inbox dismiss and sessionhub inbox snooze take a session ID or a prefix of
at least 4 characters, matched against the current inbox; an exact ID wins.
If the session isn't in the inbox, they fail with "not in the inbox".
With --since, the item's time as you saw it (RFC 3339), dismiss fails with "has a
newer event" and sends nothing if the session has changed since then.
- A dismissed item stays hidden until something new happens: the session
blocks again, reports a new
waiting_on, or finishes another turn. - A snoozed item comes back at the snooze time even if nothing new happened.
- Triage is shared: a dismiss on one machine also hides the item on the dashboard and on every other machine.
sessionhub inbox --watch keeps the inbox on screen and reads it again every 5
seconds and after every action. It needs a terminal: with stdin or stdout
redirected, it exits with an error.
| Key | Does |
|---|---|
| j or Down, k or Up | Move the cursor. |
| Enter | Jump to the session (see below). |
| d | Dismiss the item. |
| s, then 1, 4, or t | Snooze for 1 hour, 4 hours, or until 9:00 tomorrow. Any other key cancels. |
| r | Read the inbox now. |
| q or Ctrl+C | Quit. |
The bottom line shows when the list was last read, what the last action did,
and the last error. The cursor stays on its session across reads; if the
session leaves the inbox, the cursor moves to the row that took its place.
The view uses the terminal's alternate screen and raw mode (through stty)
and restores both when it exits, also on SIGINT, SIGTERM, SIGHUP, or a
crash. After a signal, the command exits with status 130 and prints no
error message.
q and Ctrl+C quit at once, even during a slow request: they cancel the read, jump, dismiss, or snooze in flight.
Enter jumps to the session under the cursor. A session with an open move
is refused, like sessionhub resume refuses it, and the status line shows the
sessionhub move --status command instead. Otherwise:
-
On this machine: the same as
sessionhub resume <id>(focus the session's pane, or start it in a pane), without printing the resume command. If there is no herdr pane to use, the status line shows the command. -
On another machine:
ssh -o BatchMode=yes <ssh_host> '~/.local/bin/sessionhub resume <id>', with a 15-second limit, so that machine's herdr focuses or starts the session.sshreads no input from the terminal and its output isn't shown, so the view keeps working.- If a local herdr tab named
sessionhub:<machine>exists, it is focused. Otherwise sessionhub creates it, focused, and runsherdr --remote <herdr_host>in it (withherdr pane run). An existing tab is focused as it is, even if itsherdr --remotehas exited. - If the server lists no such machine, the machine has no
herdr_host, orsshfails, the status line shows the resume command instead. Ifsshran butsessionhub resumeon the other machine exited non-zero, the line sayssessionhub resume on <machine> failed (<reason>). It saysssh <host> failedonly whensshitself failed (exit status 255, a timeout, or nosshbinary).BatchModemeanssshnever waits for a password: set up key authentication for the jump to work.
If your SSH config has a
Hostentry named after the machine (for exampleHost towerwith a LAN address), the jump uses that name instead of the server'sssh_host, for bothsshandherdr --remote(when the herdr host was the same as the SSH host). The server's host is shared by every viewer and may be a name, such as a Cloudflare hostname, that this machine can't reach. sessionhub checks for the entry withssh -G <machine>.
Knowledge entries are short facts, conventions, and decisions that reach
the Claude sessions they apply to. Each has a kind (convention, fact, or
decision) and a scope (global, repo:<host/path>, ticket:<ref>, or
machine:<name>). sessionhub knowledge add always creates a proposal:
nothing reaches a session until you approve it on the dashboard's
Knowledge tab. The CLI cannot approve, reject, or retire an entry,
because it runs with the machine token and a session could approve its own
proposal. add prints the proposal's number and up to 3 similar entries. The
text is one line of at most 300 characters, --why at most 200. --every N
(1 to 100) sends the entry with every Nth prompt of a session; the default is
every prompt for a convention and every 20th for a fact or decision.
Each machine's herdr watcher copies the approved entries within a minute.
See Knowledge for scopes, limits, and what a session receives.
sessionhub rules works on the global conventions, the entries that every
session sees. sessionhub rules add saves one as an active entry without a
proposal; each machine's herdr watcher copies the list
within a minute, and sessions see it from their next prompt. A rule is one
line of at most 300 characters, and the global conventions together are at most 2,000; an add
past that fails and asks you to remove a rule first. --every N makes a
periodic rule: it is sent at session start and then with every Nth prompt, a
reminder that keeps long sessions on track without repeating it each turn. The dashboard's
Knowledge tab and the MCP tools remember, forget, and instructions
edit the same entries.
sessionhub send types a message into each target session's terminal, prefixed
with From the user via sessionhub (cli on <machine>): and a blank line. Each
target is a session ID or a prefix of at least 4 characters; --machine M
targets every live session on M that can take a message: in herdr with
the watcher polling, or with the sessionhub mod polling. Flags may come before or
after the targets.
It prints one line per target, queued or refused: <reason>, then waits
up to 15 seconds and prints the delivery result of each queued one:
delivered, not delivered yet: <reason>; sessionhub keeps trying for 10 minutes
(the agent is working or waiting on a prompt), refused: <reason>, or
expired. The watcher types a message only when the session's agent is
idle or done. The command exits 1 when any target was refused or expired.
At most 30 messages a minute leave one machine.
sessionhub task keeps your task list. It acts as you, not as an agent. See
Tasks for the states.
sessionhub task add "Fix DNS for a customer" --ref "email from a customer" --source email --now
sessionhub task ls
sessionhub task start 3kQ9x
sessionhub task done 3kQ9x --note "deployed"
| Command | Moves the task to |
|---|---|
accept |
todo |
start |
in_progress |
done |
done |
drop |
dropped |
reopen |
todo |
reject |
dropped for a proposal, or in_progress for a done proposal. Any other state is an error. |
accept takes a proposal, reopen takes a done or dropped task, and
start also takes a proposal. The server refuses a move the state table
doesn't allow with 409 and says why. Flags can come after the ID.
task ls and task review print a short ID: the first 8 characters after
t_. A verb accepts a unique prefix of it, or a full ID with or without the
t_. A prefix matches the open tasks and today's columns. A task finished
on an earlier day is in neither, so task ls --date <past day> prints full
IDs, and you pass one to reopen or another verb. If a prefix matches more
than one task, the command lists the candidates.
task add fails with a conflict when an open task already has the same
--ref.
The sessionhub Claude Code mod runs inside every interactive Claude Code session in
a terminal. It reports what an AskUserQuestion asks, so the inbox and the
Telegram alert show the question, and the session's context window fill and
cost. It also types sessionhub send messages into the session, with or without
herdr, shows the inbox counts in the session's status line, such as 2 blocked · 1 waiting, and adds the /inbox command, which opens the inbox in
a pane (see The inbox pane). The mod holds no token:
it runs sessionhub mod and, for the pane, sessionhub inbox, sessionhub approve, and sessionhub deny.
sessionhub install-mod:
- Writes the mod, which the binary carries, to
~/.local/share/sessionhub/claude-mod/, with the absolute path of the installed binary (~/.local/bin/sessionhub, or--binary PATH) filled intohooks/config.js. The files go to a new directory that replaces the old one with a rename. If nothing changed, it writes nothing. - Adds that directory to
env.CLAUDE_CODE_PLUGIN_DIRSin~/.claude/settings.json($CLAUDE_CONFIG_DIR/settings.jsonwhen set, or--settings FILE). It keeps the other:-separated entries and the rest of the file byte for byte, writes asettings.json.sessionhub-backup-<time>first, and writes nothing when the directory is already listed.
If the settings edit fails, the previous mod directory is put back. The
command refuses to run until the binary is at ~/.local/bin/sessionhub
(see the README's Install).
Sessions started after the install load the mod.
sessionhub uninstall-mod removes the directory from CLAUDE_CODE_PLUGIN_DIRS
(and the key, and an env left empty, when it was the only entry), then
deletes ~/.local/share/sessionhub/claude-mod/.
The mod is part of the binary, so after you install a new binary, run
sessionhub install-mod again to update it; make deploy does it on the
server host when the mod is installed there. sessionhub install-hooks prints a reminder while
the mod is not installed.
In an interactive Claude Code session in a terminal, /inbox opens a pane
titled sessionhub inbox, and running /inbox again closes it. Close
(hotkey q) and Claude Code's own close mark close it too. Opened from an
empty prompt, the pane takes the keyboard, so the hotkeys work at once;
Esc returns to the prompt, and ctrl+x tab goes back to the pane.
The pane lists the same items as sessionhub inbox, grouped Blocked,
Waiting, and Finished. Each item shows the session title (or the
first 8 characters of its ID), the machine, and how long ago it triggered. A
blocked item also shows the question it waits on and, when it waits on a
permission prompt, the tool and one line of its input. A waiting item shows
its first three waiting_on lines. Long lines are cut to the pane's width.
Each item has buttons:
| Button | Shown for | Runs |
|---|---|---|
| Allow once | A blocked item with an open permission request | sessionhub approve --yes <request-id> |
| Deny | A blocked item with an open permission request | sessionhub deny --yes <request-id> |
| Dismiss | Every item | sessionhub inbox dismiss --since=<item time> <session-id> |
Nothing runs until you press a button. The pane shows what the command printed, or its error, as a toast, then reads the inbox again. Allow once is left out in two cases, where the pane says why instead:
- The request is for
AskUserQuestion: allowing it from outside answers nothing, so the pane says to answer in the session. - The hook cut the request's input:
sessionhub approverefuses it (seesessionhub approveandsessionhub deny), so the pane says to allow it in the terminal.
Dismiss passes the item's time with --since: if the session has a
newer event than the pane shows, nothing is dismissed and the toast says so.
While the pane is open, it runs sessionhub inbox --json every 15 seconds,
30 seconds after a failed read, and right after each action or a press of
Refresh (hotkey r). If a read fails, the pane keeps the last items
and shows the error above them. Each read also updates the status line's
counts and the cache the other sessions on the machine read them from. The
reads stop when the pane closes or the session ends. Every text from the
server is drawn as plain text, with control and bidirectional characters
removed.
The mod runs these subcommands with $.process.run. Their arguments and
input are a contract with the mod (internal/claudemod/mod/hooks/register.js)
and stay backward compatible: unknown JSON fields are ignored, and an
unknown flag is an error. Each call uses a 5-second HTTP limit except poll.
Any failure exits non-zero, with the error on stderr, and stdout carries only
the output below.
| Subcommand | Stdin | Does |
|---|---|---|
blocked-on --session ID |
{"questions": [...]} (the AskUserQuestion input) or {"text": "..."}, with "clear": true to clear |
Reports what the session waits on, as one line such as Question: Which library should we use? (date-fns, luxon, dayjs); more questions are joined with ·. A question is sent once and never queued: replayed later, it would block a session that was already answered. With "clear": true, the same input formats to the same line, and the server clears the block only while it is that question. {"text": ""} (an older mod) clears only a block the mod set. A clear that can't reach the server is queued (blocked_on_clear, with the question) and exits 0. |
usage --session ID |
{"context": {"tokens", "window", "percent"}, "cost": {"usd"}} |
Reports the context window fill (percent, else tokens over window) and the cost. It skips the call unless the fill moved by at least 1 point or the cost by at least $0.01 since the last report it sent for that session, which it keeps in <state dir>/mod-usage/<session>.json. Never queued. |
poll --session ID |
none | Waits up to 25 seconds for the session's next message, 30 seconds in all. Prints {"id": "...", "text": "..."} on one line, or nothing when none came. A transport or server error exits non-zero, so the mod backs off. |
result --message ID --state delivered|busy|refused [--detail TEXT] |
none | Reports what happened to a message poll printed. |
inbox-count |
none | Prints {"blocked": n, "waiting": n, "finished": n}. |
The state directory is ~/.local/state/sessionhub, or $SESSIONHUB_STATE_DIR. State
files of sessions that have not reported for 7 days are removed.
When a session waits on a permission prompt, sessionhub inbox shows
asks to use <tool>: <command or file> on its Blocked line, with
(cut) when the hook cut a long input. sessionhub approve <prefix> allows it
once, and sessionhub deny <prefix> ["reason"] denies it; Claude sees the reason.
The target is the session ID or a prefix of at least 4 characters, matched
against the inbox, or the request ID (pr_..., listed by sessionhub inbox --json).
For a session prefix, the command first prints what you answer: the
session, the tool, the whole Bash command (or, for any other tool, its
whole stored input as compact JSON, at most 8 KiB), line by line and
indented four spaces so every line of a multi-line command shows, and the
request ID. It
then asks Allow this once? [y/N] (or Deny this request? [y/N]) and
sends nothing unless you type y or yes. If stdin is not a terminal,
the command refuses unless you pass --yes, which answers without asking.
A request ID skips the question: you name the exact request.
If the hook cut a long input, you cannot see all of it, so sessionhub approve
prints the cut note and exits 1 with "the input was cut; allow it in the
terminal", even with --yes or a request ID (the server refuses that
allow with 409, and the command prints its reason). sessionhub deny still
works on a cut input.
The answer reaches the session within about a second. If someone answered already, the command fails with "already decided"; if the prompt was answered in the terminal or expired, it fails with the server's reason. sessionhub only ever allows once; it never adds a permanent rule.
sessionhub move <id-or-prefix> tower moves a session to the machine tower, with
its conversation, its branch, and its uncommitted changes, and resumes it
there in a new herdr workspace. sessionhub move <id-or-prefix> cloud hands it to
a new Claude Code cloud session instead (GitHub remotes only). You can run it
on any machine.
The session must be in herdr on a machine whose watcher runs, and its agent
must be idle or done. The target machine needs a running watcher and a clone
of the same repository under ~/Code (or a move_roots entry in
~/.config/sessionhub/config.toml) with no uncommitted changes, and the same Claude
Code version. When a rule fails, the server says which, and nothing moves.
It prints one line per state:
mv_... moving 3f2a9c10 (auth work) from bluebox to tower
requested: waiting for the sessionhub watcher on bluebox
packing on bluebox: ending the session and sealing the bundle
uploaded (3.0 MiB): waiting for tower
unpacking on tower
done: the session runs on tower now; not carried: .env
see notes later with: sessionhub move --status mv_...
A failure prints failed: <step>: <reason> and exits 1; the session then
runs again on the source machine. After done or failed, the last line
prints the sessionhub move --status command: the source may add a note to the
move later, such as an archive or a restart that failed there. After 3 minutes the command stops
following and prints the sessionhub move --status command to check later. If you press Ctrl+C, the move goes on, and the command prints the same hint and exits 130. Files
listed as "not carried" (.env*, *.pem, *.key, *.tfvars) stay on the
source; copy them by hand.
sessionhub move-key on each machine and sessionhub machine ls on the server show the
same fingerprint for a machine. If they differ, someone swapped a key on the
server: don't move sessions until you find out why.
sessionhub start <machine> [--dir DIR] [--trust] [-m "first prompt"]
Asks <machine>'s watcher to start a new Claude session with Remote Control
on, in a new herdr workspace in DIR, and follows the request for up to
2 minutes 10 seconds:
st_roa-9PDiUM1O9kEUXQZxZQ starting a session on tower in ~/Code/app
waiting for the sessionhub watcher on tower
starting Claude on tower
started on tower: https://claude.ai/code/session_01... (started in a new herdr workspace)
-
--dirdefaults to the current directory when<machine>is this machine, and to~(that machine's home directory) otherwise. It must be absolute or start with~/, and on the machine it must exist and be inside the home directory. -
Claude must already trust
DIR, or a folder above it, on that machine. Otherwise the watcher starts nothing and the request fails:failed: /home/me/Code/new is not trusted by Claude on tower; open it once or pass --trustTo trust it, run
claudein that folder once and accept the prompt, or pass--trust. -
--trustmarksDIR, and onlyDIR, as trusted in Claude's config on that machine (projects["DIR"].hasTrustDialogAcceptedin~/.claude.json, or$CLAUDE_CONFIG_DIR/.claude.json) before starting. It never trusts a folder aboveDIR. It refuses a relative--dirand the home directory, including the~default on another machine, because trusting home trusts every folder under it. The watcher rewrites the config atomically and keeps every other setting. -
-mis a first prompt, submitted once Claude is idle. If Claude is still waiting on a prompt in the pane, the prompt is not sent, and the result says so. -
It exits 0 once the session started, 1 when the request failed or expired, and 130 when you press Ctrl+C, which stops following but not the start.
-
A machine whose watcher is offline, an unknown machine, or a machine with
remote_start = falserefuses with the reason.
sessionhub digest <id> reads one session's transcript and git activity on this
machine and sends its digest to the sessionhub. sessionhub digest --all does this for every
session the sessionhub lists for this machine, which backfills sessions that started
before you installed digests. <id> is the full session ID or a unique prefix, such as the 8 characters sessionhub ls prints.
Each session prints one line, <first 8 characters of the ID> <result>:
| Result | Meaning |
|---|---|
sent |
The sessionhub accepted the digest. |
queued |
The sessionhub was unreachable or busy (network error, timeout, 408, 429, or 5xx). The digest waits in the queue and the next drain sends it. |
skipped: <reason> |
Nothing to send: no transcript, another run is in progress, or nothing to report yet. |
failed: <reason> |
The sessionhub rejected the digest (for example, it doesn't know the session) or the transcript has too many unreadable lines. Failed digests aren't queued. |
--all ends with a total line, N sent, N skipped, N failed, where queued
counts as sent. The command exits non-zero only when a digest failed. Running
--all again is safe: each digest replaces the previous one. See
Digests.