rcman
v1.2.0
Published
Remote Control Manager — a PM2-style supervisor for AI coding-agent remote-control sessions (Claude Code, OpenAI Codex CLI).
Maintainers
Readme
Follow me on X @ybouane — I'm building in public.
rcman — Remote Control Manager
A PM2-style process manager for AI coding-agent remote-control sessions.
systemd manages rcman. rcman manages your agent sessions.
$ rcman list
┌────────────┬──────────┬────────┬───────┬────────────┬──────────────────────────┬────────┐
│ Name │ Provider │ Status │ PID │ Session ID │ Directory │ Uptime │
├────────────┼──────────┼────────┼───────┼────────────┼──────────────────────────┼────────┤
│ storefront │ claude │ online │ 16549 │ d7b1e2a4 │ /srv/projects/storefront │ 4h12m │
│ api-server │ claude │ online │ 16712 │ a1c9f082 │ /srv/projects/api-server │ 4h11m │
└────────────┴──────────┴────────┴───────┴────────────┴──────────────────────────┴────────┘No tmux. No git worktrees. rcman never touches your repository, your branches or your agent conversations.
Why
Remote control lets you drive a coding agent running on your server from your phone or a browser. Keeping a dozen of those alive by hand is the problem: they die on reboot, they stall on a startup modal, and a second attach to the same session forks its history.
rcman is the supervisor for that: one managed process per session, restarted when it crashes, restored after a reboot, with per-session logs and a registry of exactly which session id lives where.
Install
npm install -g rcman
rcman service install # detects this machine's service manager and registers rcmanservice install figures out how the machine starts things at boot — a systemd
user service, a launchd agent, an OpenRC service, or a cron @reboot entry —
writes the right unit with real resolved paths, enables it and starts it. On
systemd it also turns on user lingering, without which nothing survives a
logout. rcman service detect shows what it found and why.
Requires Node 18.17+, and at least one provider CLI on PATH
(Claude Code or the
Codex CLI).
Quick start
rcman setup # guided: provider, boot service, first sessionssetup checks whether an agent CLI is installed and offers to run its own
installer if not — showing you the exact command first, defaulting to no. It
then checks you are signed in, and offers to run the provider's login flow. Then
the boot service, the daemon, and your first sessions.
Or do it by hand:
rcman config set defaultProvider claude # used when --provider is omitted
rcman start my-session ~/projects/foo # create a new session in that directory
rcman start my-session # resume the one you already have
rcman ls # what is running
rcman logs my-session -f # follow its output
rcman info my-session # session id, cwd, remote URL, limitations
rcman restart my-session
rcman stop my-session
rcman remove my-session # unregister; the conversation survivesHow rcman start decides
rcman start <name> <path> -> create and start a NEW session there
rcman start <name> -> resume an EXISTING one:
1. rcman's registry, by name
2. otherwise ask the provider adapter to
discover a local session with that name
3. most recently active wins if several match
4. register it, then resume that exact sessionDiscovery reads only the provider's own session metadata (~/.claude/projects,
~/.codex/sessions). It never walks your project directories.
If neither the registry nor discovery finds the name, rcman stops and says so. It will not quietly create a new session behind a resume request.
Commands
| Command | What it does |
| --- | --- |
| rcman setup | Guided first-run: install a CLI, sign in, register for boot, add sessions |
| rcman start <name> [path] | Start a session (see the rules above) |
| rcman start all | Start every enabled session |
| rcman start <file.json> | Register and start everything in an ecosystem file |
| rcman stop <name> / stop all | SIGTERM, then SIGKILL after a grace period |
| rcman restart <name> / restart all | Stop, then start |
| rcman remove <name> | Unregister from rcman only |
| rcman enable <name> | Autostart on; start it now |
| rcman disable <name> | Stop it and keep it stopped across restarts |
| rcman list, rcman ls | Status table |
| rcman status <name> | One-line status |
| rcman info <name> | Everything rcman knows + provider limitations |
| rcman logs [name] [-n N] [-f] | Per-session logs |
| rcman discover [name] | Sessions the provider knows about locally |
| rcman providers | Installed provider CLIs and their limitations |
| rcman config get/set/unset/list | Settings |
| rcman reload | Re-read config + registry; running sessions keep running |
| rcman trust <path> / untrust | Let the provider act in a directory |
| rcman service install | Auto-detect the service manager and register for boot |
| rcman service status | Where rcman is registered, and whether it is running |
| rcman service detect | Which managers exist here, and which one wins |
| rcman service print | Show the generated unit without installing it |
| rcman service uninstall | Deregister (sessions are not deleted) |
| rcman daemon | Run the supervisor (what the service runs) |
| rcman daemon --stop | Stop the supervisor; sessions keep running |
| rcman upgrade | After an npm update: refresh the unit, restart the daemon |
Add --json to list, status, info, discover for machine-readable output.
Configuration
Global settings live in $RCMAN_HOME/config.json (~/.rcman by default):
rcman config set defaultProvider claude
rcman config get defaultProvider
rcman config listSessions are declared in a PM2-style ecosystem file kept outside the package:
{
"sessions": [
{
"name": "my-session",
"provider": "claude",
"sessionId": "d7b1e2a4-9c3f-4f56-b0aa-6e1c2f3d4a5b",
"cwd": "~/projects/foo",
"enabled": true,
"providerOptions": {"resumeMode": "summary"}
}
]
}rcman start ~/rcman.jsonEdit config.json or registry.json directly and rcman picks the change up on
its own — or rcman reload to apply it now. Either way running sessions keep
running: supervision settings and notes are applied in place, and an edit that
changes how a session launches is reported as pending rather than acted on,
because saving a file is not permission to interrupt an agent mid-task.
Full field reference: docs/CONFIGURATION.md.
Updating
npm install -g rcmannpm replaces files; it does not restart processes. The daemon would otherwise keep executing the version it started with, while the CLI you just installed is the new one — talking to it over an IPC contract that may have moved. rcman handles that in three ways:
- a post-install hook restarts the daemon when one is running and the
version actually changed. It is inert for a non-global install, inert when no
daemon is running (so image builds and CI do nothing), never fails the
install, and
RCMAN_NO_POSTINSTALL=1turns it off. - every command notices the skew anyway, because the handshake it already makes now carries a version, and says so rather than letting you find out sideways.
rcman upgradedoes it deliberately: refresh the boot unit if the install moved, restart the daemon, and confirm every session came across.
$ rcman upgrade
rcman 1.0.2 · daemon running 1.0.1
✔ daemon restarted on 1.0.2 (pid 129203)
✔ 8 session(s) carried across untouchedThis is only safe to do automatically because a managed session outlives the supervisor — see Sessions outlive the supervisor.
rcman upgrade also catches a failure npm cannot: service install bakes
absolute paths for node and rcman's entrypoint into the unit, and under a
version manager both live under the node version. Installing rcman on a newer
node leaves the unit pointing at the old copy, which nothing surfaces until the
next boot.
Providers
rcman talks to agent CLIs through adapters. Two ship today; the contract is one
file (src/providers/base.js) and adding one is an import plus a line in
src/providers/index.js.
Claude Code
Fully supported: one managed process per session, bound to one exact session id.
claude remote-controlis an environment, not a session. The subcommand starts a persistent server that hosts up to--capacityconversations created on demand, and publishesclaude.ai/code?environment=env_…. The flag on the main command —claude --remote-control— is one session, publishingclaude.ai/code/session_…. rcman supervises sessions, so it uses the flag. The environment is available asproviderOptions.attach = "environment".- A new session gets its uuid from rcman up front (
--session-id), so every later start resumes that same conversation instead of beginning a new one. claude remote-control --session-id <id>only reattaches a bridge session (acse_…/session_…id that already has an environment). Given a local transcript uuid it fails with "has no environment_id", so it is not the way to resume an ordinary local session. rcman exposes it asproviderOptions.attach = "bridge".- Binding remote control to one exact local session therefore uses
claude --resume <uuid> --remote-control, which is the interactive UI and refuses to run on a pipe. rcman allocates a pty withscript(1)— no tmux, no native dependency. - Resuming a large session shows a blocking "resume from summary / full"
modal. rcman answers it (summary by default;
providerOptions.resumeMode). - The workspace must be trusted before a session can run there.
claude remote-controlhard-errors on an untrusted directory rather than prompting, and the resume path shows a modal that stalls forever — so rcman reads~/.claude.jsonup front. In a terminal it offers to grant it there and then; otherwise it refuses with the fix,rcman trust <path>. Either way it is a decision you make — never a side effect of starting a session. - Reattaching mints a fresh bridge, so the claude.ai/code URL changes on every
restart.
rcman info <name>always has the current one.
OpenAI Codex CLI
Supported with a documented ceiling, because Codex's architecture differs.
- Codex remote control is one machine-wide app-server daemon, not a per-session process. rcman supervises exactly one Codex session per machine and refuses a second.
- There is no
--session-idequivalent: remote control cannot be bound to a specific thread. Threads are created and attached from the Codex app. codex resume <id|name>does resume a specific thread, but it is the interactive TUI and is not a remote-control surface.- The daemon self-backgrounds, so rcman's managed child is a small supervisor
that keeps it alive and healthy and stops it on
rcman stop. - Pair once, interactively:
codex remote-control pair.
rcman providers prints the current list for the CLIs you actually have
installed.
Process management
- One managed child process per session, tracked by PID, status and start time.
- Crashed sessions restart with a delay and a crash-loop budget; a session that ran happily for hours always gets a fresh allowance.
- Graceful stop: SIGTERM to the process group, SIGKILL after
stopTimeoutMs. - rcman never signals a process it did not start. Every kill is gated on the
live
/proc/<pid>/cmdlinestill matching the command rcman recorded, and leftovers are only swept if they were both descendants of that process and still a signature match. onlinemeans reachable, not merely alive. A provider declares whether a healthy session publishes a remote-control URL; when one that should has not after 90s, rcman says so rather than implying all is well. (A session stalled on an unanswered startup prompt keeps its process and passes every other check, while never appearing in the app.)- Startup modals that would stall an unattended session are answered from a list
the provider declares — the resume prompt,
Enable Remote Control?. Security decisions are never among them: those are reported, or asked about. - Duplicate protection: rcman refuses to attach to a session another process (a terminal, an IDE, another supervisor) is already driving, because two writers fork a conversation's history.
- Session-scoped environment markers from a parent agent process are scrubbed —
inheriting
CLAUDE_CODE_CHILD_SESSIONsilently disables transcript saving. - Per-session logs, ANSI-stripped, spinner-frame-filtered and size-rotated.
Sessions outlive the supervisor
Restarting rcman must not cost you an agent's train of thought. Upgrading the package, editing the config, a daemon crash — none of them touch a running session. Three things make that true, and each one is load-bearing on its own:
- No pipes. A child whose stdout is a pipe to the daemon is killed by SIGPIPE the moment the daemon exits. Sessions write to plain files instead, and the daemon tails them — so a successor resumes capture mid-stream, at the exact byte the last one reached, rather than replaying or losing output.
- A fifo for stdin, opened
O_RDWR. The child inherits a descriptor that is also a writer, so its stdin never reaches EOF even with no daemon alive to hold it. A new daemon reopens the write end and can answer a prompt again. - Its own cgroup.
systemctl stopkills a cgroup, not a process, so a session spawned insidercman.servicedies with it whatever the daemon does. rcman launches each session in its own transient scope viasystemd-run, and the generated unit setsKillMode=processas a second line of defence.
So SIGTERM to the daemon means detach, not stop: it persists each pid,
signature and capture offset, lets go, and the next daemon adopts them. Ending a
session stays an explicit act — rcman stop, or rcman daemon --stop
--with-sessions.
systemctl --user restart rcman # sessions never notice
rcman daemon --stop # supervisor goes, sessions stay
rcman stop all # this is how you actually stop themStarting at boot
rcman service install # auto-detect and register
rcman service status # where it is registered
rcman service detect # what is available on this machine
rcman service print # the generated unit, on stdout
rcman service uninstall # deregister
rcman service install --manager cron # override the auto choiceManagers are tried in this order, and the first usable one wins:
| Manager | Used when | Restarts on crash |
| --- | --- | --- |
| systemd-user | Linux with a systemd user manager | yes |
| launchd | macOS | yes |
| systemd-system | Linux, running as root or with passwordless sudo | yes |
| openrc | Alpine and friends, with root | yes |
| cron | anything else with crontab | no — boot only |
The generated unit hardcodes nothing about your machine: node's path, the rcman
entrypoint, RCMAN_HOME and a minimal PATH containing the provider binaries
are all resolved at install time. Adding a manager is one file in
src/service/ plus a line in src/service/index.js, exactly like a provider.
Runtime layout
Nothing is written inside the installed package. What you edit and what rcman maintains are deliberately separate files:
$RCMAN_HOME/ # ~/.rcman by default
├── config.json # global settings — watched for edits
├── registry.json # registered sessions — watched for edits
├── state.json # pid, signature and capture offset, for adoption
├── daemon.sock # CLI <-> daemon
├── daemon.pid
├── daemon.log
├── logs/
│ ├── <name>-out.log # readable: ANSI-stripped, denoised, rotated
│ └── <name>-err.log
└── run/
├── <name>.in # stdin fifo, outlives the daemon
└── <name>.out.raw # what the child writes; the daemon tails thisRCMAN_HOME moves all of it. RCMAN_CLAUDE_BIN / RCMAN_CODEX_BIN point at a
specific CLI binary.
Architecture
systemd --user
└── rcman daemon one supervisor process — disposable
├── unix socket newline-delimited JSON, CLI <-> daemon
├── registry.json which sessions exist; watched for edits
└── ManagedSession × N one child process each, in its own cgroup scope
├── stdio files + a fifo, so the child outlives the daemon
└── provider adapter builds the command, discovers sessions,
answers startup modals, reports limitssrc/core/— daemon, IPC, registry, config, process control, logging.src/providers/— one file per agent CLI, plus the adapter contract.src/service/— one file per boot-time service manager, plus its contract.src/commands/— one function per CLI verb.
The CLI is a thin client: it auto-starts the daemon if it is not listening, then sends a single request.
Development
npm test # node:test, no dependencies
node bin/rcman.js helprcman has zero runtime dependencies.
License
MIT
