ccs keeps named CLI-agent sessions — Claude Code and Codex CLI — running on a Linux host so that
several terminals can attach to the same session at once and stay in sync, each with the agent's full
rendering in its own native terminal scrollback. Detach a terminal, close your laptop, come back
tomorrow from a different machine — the session keeps running. The official Claude phone app keeps working
through Remote Control (Claude sessions only — see Hosting Codex CLI sessions).
terminal A ─┐
terminal B ─┼─▶ ccs-sessiond ─▶ ccs-holder ─▶ claude | codex (one process, one conversation)
phone (RC) ─┘ (emulator, (owns the pty)
per-client view)
Unlike tmux or screen, ccs does not own your scrollback. Each client renders into its own terminal,
so you scroll with your normal mouse wheel, trackpad, and scrollbar — no prefix key, no copy mode.
Status: v0.2.0. Adds Codex CLI hosting alongside Claude Code, on the same terms: persisted and
displayed session type, honest (never fabricated) status, thread-id discovery and resume, per-agent
preflight and doctor checks — with zero changes to Claude's own code paths. The automated suite (680
tests: 495 unit, 93 integration, 92 golden) and the live suites against real claude and codex binaries
pass. See Limitations for what is not built yet.
- Linux (uses
/proc, AF_UNIX sockets, and a small native TTY addon) - Node.js 22.x — one interpreter, consistently: the native addons are compiled for its ABI, and Codex
support additionally needs Node's built-in
node:sqlite(Node 22.5+) - gcc, make, python3 —
node-gypbuildsnode-ptyand the bundledttyctladdon from source - Claude Code installed and logged in. Tested against 2.1.267; other versions run but report
UNTESTED_CLAUDE_VERSION(a warning, never a block) - Codex CLI — optional. Tested against 0.155.0/0.155.1; other versions run but report
UNTESTED_CODEX_VERSION. Codex not being installed is never a doctor failure — it's simply unavailable until you install it
curl -fsSL https://raw.githubusercontent.com/kaiwang13/AgentServer/main/install.sh | shThat clones and builds a release under ~/.local/share/ccs/releases/, points ~/.local/share/ccs/current
at it, and puts a ccs wrapper in ~/.local/bin. Then:
ccs doctor --lite # fix every FAIL before going furthercurl -fsSL https://raw.githubusercontent.com/kaiwang13/AgentServer/main/install.sh | sh -s -- updateYour sessions survive an update — you never recreate them. Session records live in
~/.local/state/ccs, outside the install, and are never touched. The update builds the new release
completely before anything stops, so a failed build changes nothing; then it stops the sessions that
were running, switches the current symlink, and starts exactly those again with --resume, so each
conversation continues where it was.
Releases are immutable directories, so rolling back is one symlink:
ln -sfn ~/.local/share/ccs/releases/<previous> ~/.local/share/ccs/currentA running session holds the native addon mapped from its release directory, which is why an in-place
git pull on a live install breaks it — and why updates go through a new directory instead.
Installing, updating or uninstalling never touches your session records. Your sessions are your work. An update may stop and restart them; it is never allowed to lose, rewrite or orphan a record. This holds for every future feature too — new session kinds, new state files, new registry fields.
It is enforced by a test rather than a promise: test/e2e/install-e2e.sh fingerprints the registry (every
file's path, size and content hash), runs a full install → update → uninstall in a throwaway prefix, and
asserts the tree is byte-identical afterwards. It also plants a session record and checks it survives
uninstall unchanged.
sh test/e2e/install-e2e.sh # against the published main branch
CCS_E2E_REPO=$PWD sh test/e2e/install-e2e.sh # against your working copyAn install is self-contained: everything lands under $HOME, nothing needs sudo, and setting
CCS_HOME, CCS_BIN_DIR and CCS_STATE_DIR gives you a fully isolated second install that cannot see or
stop the sessions of the one you use — which is what makes that test safe to run on a live machine.
curl -fsSL https://raw.githubusercontent.com/kaiwang13/AgentServer/main/install.sh | sh -s -- uninstallStops running sessions and removes the install and the wrapper. Session records are kept so a
reinstall picks them up; add --purge to delete them and the config too. Claude's own transcripts in
~/.claude/projects are never touched by any of these commands.
git clone https://github.com/kaiwang13/AgentServer.git ccs && cd ccs
npm ci && npm run build:native && npm testThen point a wrapper at bin/ccs.js with an absolute node path — sessions outlive your shell, so a
daemon restarted later must not depend on nvm being loaded. UV_THREADPOOL_SIZE=16 matters if your home
is on NFS: a stalled server parks a libuv thread in uninterruptible sleep, and the default pool of 4 is
easy to exhaust.
#!/bin/sh
export UV_THREADPOOL_SIZE=16
exec /absolute/path/to/node /absolute/path/to/ccs/bin/ccs.js "$@"Stop every session before rebuilding a clone you are running from (ccs stop --all): live processes hold
the addon open, and replacing it under them fails.
cd ~/my-project
ccs new myproject # creates the session and attachesFrom a second terminal — another SSH session, another editor, another machine:
ccs a myproject # same session, in sync, in your own scrollbackCtrl+Z or Ctrl+\ detaches the terminal you are in; the session keeps running. Bare ccs opens a
picker of your sessions.
| Command | What it does |
|---|---|
ccs |
Session picker on a TTY (↑↓ move, ⏎ attach, n new, s stop, q quit); ccs ls otherwise |
ccs new [NAME] [--cwd DIR] [--no-attach] [--agent claude|codex] [--resume ID] [-- --model opus] |
Create a session and attach. --agent defaults to claude (or config.defaultAgent) |
ccs a|attach [NAME] [--shrink] [--allow-setup] |
Attach; starts a stopped session first |
ccs ls [--json] |
List sessions — a TYPE column shows claude or codex |
ccs stop NAME|--all |
Stop cleanly — /exit when idle, then SIGTERM. claude is never force-killed |
ccs restart NAME |
Stop and start again with --resume; clears FAILED/BLOCKED/EXITED |
ccs rm NAME |
Remove a stopped session. Claude's transcript is kept |
ccs doctor --lite [--accept-config] |
Health checks |
Width is shared. All attached clients see one terminal, sized to the narrowest one. Prefer a full
editor-area terminal over a narrow side panel, or attach the small one with ccs a NAME --shrink.
Session names are visible to other users. On a host whose /proc is mounted without hidepid, any
account can read claude -n NAME from /proc/<pid>/cmdline. The default name is therefore an opaque
ccs-<id6>; passing a readable name prints a one-line reminder. Names are a-z 0-9 . _ -, up to 40
characters.
/exit detaches, it does not stop. Typing /exit or /quit disconnects only your terminal; anyone
else stays attached. Use ccs stop NAME to end a session.
Trust dialogs. In a directory Claude has not trusted, Claude shows its trust dialog. ccs never
answers it and never writes ~/.claude.json; only a client attached with --allow-setup can type into it,
which ccs new does automatically for an untrusted directory.
cd ~/my-project
ccs new myproject --resume <session-uuid>Find the UUID for a directory — Claude stores transcripts per project path:
D=~/.claude/projects/$(readlink -f "$PWD" | sed 's|[/_]|-|g')
ls -t "$D"/*.jsonl | head -5 | xargs -n1 basename -s .jsonlDo not resume a conversation that is already open in a plain claude: two writers on one transcript is
not a supported configuration.
Codex is hosted the same way Claude is — attach/detach, multi-client sync, and native scrollback all work
identically. Create one with --agent codex:
cd ~/my-project
ccs new myproject --agent codexccs ls shows it with TYPE codex and an honest activity line (active Nm / idle, derived from Codex's
own thread database) rather than the working/idle/waiting detail Claude sessions get — Codex has no
per-session status file to read, so ccs never fabricates one.
Resuming an existing Codex conversation works too, but the id has to already exist in Codex's own
thread database (~/.codex/state_5.sqlite):
cd ~/my-project
ccs new myproject --agent codex --resume <thread-id>Find a thread's id from inside Codex with /status (it prints Session: <uuid>), or list every thread
ccs can currently resume:
node -e "
const {DatabaseSync}=require('node:sqlite');
const db=new DatabaseSync(process.env.HOME+'/.codex/state_5.sqlite',{readOnly:true});
for (const r of db.prepare('SELECT id,cwd,name FROM threads ORDER BY CAST(updated_at_ms AS INTEGER) DESC LIMIT 10').all())
console.log(r.id, r.cwd, r.name||'');
"A freshly opened Codex session is not resumable yet: Codex only writes a thread to its database once
there has been real conversation activity — opening it, running /status, or switching models does not
count. Send at least one real message first.
- Detach with Ctrl+Z or Ctrl+\, same as Claude — but it matters more here. Claude's
/exitis specially intercepted byccsas "detach only"; Codex has no such interception, so typing an exit command goes straight through and quits the real process. The conversation itself is never lost (it's still a real Codex thread —ccs restart NAMEresumes it), but the running session ends. --sandbox,--ask-for-approval,--profile,--searchand--modelare the extra flags allowed after--by default (config.codex.extraArgsAllow) — Claude's allowlist does not apply.- No Remote Control. Codex's remote-control is a separate daemon (
codex remote-control start), not a per-session flag like Claude's, so the phone app does not currently reach accs-hosted Codex session. - Terminal colours.
ccs new/ccs restartcapture your terminal's real default colours once at session start and forward them to Codex, so its input-box tint matches what you'd see running it natively. A session created before this existed picks it up on its nextccs restart.
Each session is a pair of processes hosting one agent (claude or codex), chosen through a small agent
adapter (src/agents/) so the two never share code paths — the holder and sessiond below are agent-neutral:
ccs-holderowns the agent's pty and nothing else. It never touches NFS, drains output unconditionally, and answers terminal queries, so the agent can never block on a slow or absent client.ccs-sessiondis restartable. It runs a headless terminal emulator over the output stream and keeps a per-client view, so a joining client receives a snapshot of the current screen rather than a replay.
Clients talk to sessiond over AF_UNIX sockets — there are no TCP listeners anywhere. Session state
lives in ~/.local/state/ccs/, runtime sockets in /tmp/ccs-<uid>/ (0700, never on NFS).
Crashed sessions are restored automatically; neither agent is ever sent SIGKILL, and a test enforces that
the string appears nowhere in the source.
For Codex specifically: ccs never writes to ~/.codex/ — src/codex/threads.js is the only module that
reads Codex's own (undocumented, versioned) thread database, always read-only, and degrades to "unknown"
on any schema surprise rather than failing a session.
Optional, at ~/.config/ccs/config.json (override with CCS_CONFIG):
{
"defaultAgent": "claude",
"claude": { "extraArgsAllow": ["--model", "--permission-mode", "--add-dir"] },
"codex": { "extraArgsAllow": ["--model", "--sandbox", "--ask-for-approval", "--add-dir", "--profile", "--search"] },
"size": { "floorCols": 50, "floorRows": 15 },
"env": { "passthrough": ["MY_VAR"] }
}Proxy variables (http_proxy, https_proxy, no_proxy, …) and CA-certificate variables are forwarded to
claude by default. The environment is snapshotted when the session is created and replayed on every
restart, so a session created in a shell without your proxy set will never have it.
CCS_PINNED_NODE pins the interpreter doctor expects, for hosts that keep several around.
npm test # unit + golden + integration (680)
npm run test:unit
npm run test:golden # terminal-emulator differential tests
npm run test:integrationNo test starts a real claude or touches your real ~/.claude*, ~/.local/state/ccs or ~/.config/ccs.
The live suites do start a real claude and are gated behind CCS_LIVE=1. They back up
~/.claude.json first, run one session at a time, and verify afterwards that nothing shrank. Read
docs/RUNBOOK.md before running them.
Not built yet (planned for later milestones): ccs start, logs, kick, rename, install with
immutable releases and cron reconcile, the full doctor, and the shell shim. ccs currently runs straight
from a clone.
Known gaps in this release:
- The extra-argument allowlist checks flag tokens but cannot tell a stray bare word from a variadic flag's
value, so a trailing word after
--can become the agent's initial prompt.ccs new --no-attachwarns. - A very long status hint in
ccs lscan push the trailing columns off the row. - Codex has no Remote Control (the phone app) and no per-session live status (
ccs lsshows honest activity instead of working/idle/waiting) — both are architectural, not planned for a later milestone. - The session picker (
ccs, bare) has no way to choose--agent codexfor a new session; useccs new --agent codexfrom the command line.
Apache-2.0 — see LICENSE.