Reference files for production deployment (public VPS relay + wrapper on your machine). Step-by-step installation paths are in 安装与升级 / Installation and upgrades. Product features and the engine comparison live in the main README.
The portable agent entrypoint is
.agents/skills/cc-remote-deploy/SKILL.md.
Both AGENTS.md and CLAUDE.md link it for clients without automatic skill
discovery. This document remains the deployment source of truth.
This directory is the deployment source of truth for humans and automation. Machine inventory is deliberately external: host aliases, usernames, domains, addresses, home directories, and credentials belong to the operator's environment, not this repository. Replace documented placeholders only with values the operator supplied or that were read from the existing installation; never guess them.
Before changing a live service:
- Inspect the source worktree, target installation, current release, service manager, and health. Preserve unrelated changes; do not normalize a dirty worktree or silently replace a custom installation layout. Apply backup retention before creating another deployment backup or staging copy.
- Select the matching supported path. Prefer a tested source snapshot for
current features using the source deployment guide.
Use
install.shwhen the operator selects a published release that includes the requested features; a latest tag can lag the maintained source branch. An existing nonstandard installation must retain its established service ownership and configuration boundaries rather than being overwritten with a first-install template. - Run the complete gate in
AGENTS.md, buildweb/dist, and validate the Python/Web protocol pair withvalidate_protocol_bundle.py. - Freeze those tested bytes once. Every Relay, Web client, and Wrapper in the maintenance window must come from that same snapshot or coordinated artifact set. Do not rebuild independently on different hosts.
- Stage and validate every target before activation. Keep
.env, device authority, profile configuration, private databases, and other runtime state outside immutable release trees. Never upload secrets as part of a source snapshot.
For Claude, follow session-service installation and acceptance
before planning a non-interrupting Wrapper upgrade. Keep an existing SDK service
outside the Wrapper activation transaction. First migration, remaining
in-process turns (including private /btw forks), and deferred queries must
drain first; daemon readiness alone does not prove an old child was adopted.
If the local service protocol or pinned SDK changes, stage first and defer the
service's own restart until native work and pending callbacks have finished.
Activate a coordinated protocol change in the order documented by the current
protocol note below: stop incompatible old Wrappers, activate Relay + Web as one
transaction, then activate/start every Wrapper and hard-refresh clients. Use the
repository installers' immutable releases/ plus atomic current switch; never
overlay the live tree with rsync --delete. If Wrapper state requires a schema
snapshot, create it while the Wrapper is stopped and keep it with the previous
release.
A command that loses SSH, terminal, or cc-remote connectivity has an unknown
result, not a failed result. Inspect the exact service/job, current target,
logs, health endpoint, PID, and restart count before retrying. Never start a
second installer merely because the first caller stopped receiving output.
A Wrapper must not be its own only deployment controller: activate it from an
independent terminal/SSH connection or from exactly one OS-owned one-shot job
that can finish after the old Wrapper exits.
Success requires all of the following: the expected immutable releases are active, Python and served Web build metadata report the same protocol/product, services have stable PIDs without restart loops, the public health endpoint is healthy, expected Wrappers reconnect, and recent logs contain no new fatal errors. These checks must pass after the final cleanup, not just before it. Installations using Codex Code must also verify the shared CLI control plane; an online Wrapper alone does not prove bidirectional CLI access. After these checks, offer the optional Codex App attachment on eligible desktops. Its consent/availability is reported separately and never turns a healthy core deployment into a failure. On failure, use the installer-owned rollback or the retained previous release and matching state snapshot. Never prune the active transaction's rollback set during activation or recovery; remove older unreferenced generations beforehand and finalize retention after coordinated acceptance as specified below.
Agent-led deployments retain the active installation, one complete previous rollback generation, and every still-referenced runtime dependency on each in-scope host. Runtime dependency protection always overrides the generation count; an older release is not disposable merely because two newer ones exist. A generation includes the matching release code/runtime, configuration copies and private state snapshot needed to restore it; these are one recovery set, not separate allowances for multiple historical copies. An intact immutable release can serve as the code backup without another archive of the same tree.
- Before creating the next backup or staging copy, identify the active release,
the newest complete known-good rollback set, and any unresolved deployment
transaction. Remove only confirmed older, superseded deployment backups and
unused duplicate uploads/archives using
deploy/cleanup.py. Determine generations from release and transaction records, not filename age alone. Preview an explicit private inventory before applying it; do not generate a separate retention script or bypass a deferred result withrm/rmtree. - Create and validate the new pre-upgrade snapshot using the normal transaction procedure. Keep the existing valid rollback set until coordinated acceptance succeeds. Temporary coexistence during this transaction must not become permanent retention; never delete the sole usable backup to make room for an unverified replacement.
- After all protocol tiers pass acceptance and the transaction is committed, retain only the version just superseded and its matching recovery files. Remove the older rollback generation and completed, unneeded staging/upload copies. Quarantine candidates, repeat acceptance while their bytes remain recoverable, then delete and verify again. On failure or unknown outcome, preserve the exact transaction's recovery set and settle it before further cleanup or deployment attempts. Deployment is complete only after this final acceptance; earlier health checks are provisional.
Do not delete native transcripts, credentials, current private state, project
files or unrelated user backups under this policy. A release still referenced
by a running service (including the independent Claude service), a process cwd,
an executable, an open/mapped file, a shared venv, an active job or the retained
rollback set is a live dependency. Checking ps ... args alone misses processes
whose argv is independent of their cwd. Include dormant service definitions and
configuration-dependent runtime paths explicitly as protected dependencies too.
Incomplete process visibility or uncertain provenance defers cleanup. Do not
stop a daemon or active work just to meet the retention count.
Codex lifecycle commands use the service user's stable home directory, never a Wrapper release, as their working directory. Existing daemons are not restarted to apply this: retain any old release they still use until their normal lifecycle has moved them away. This startup rule and the cleanup checks protect different boundaries; neither replaces the other.
Report retained rollback paths, deleted generations, removed allocated bytes and any deferred paths. Allocated bytes are not a measurement of free-space gain (for example, shared filesystem blocks may remain in use). Moving old copies into another backup directory or Trash does not reclaim disk space or satisfy this policy.
Managed Release installers from v4.0.10 apply this policy automatically
after activation and registration. release_retention.py records the exact
artifact identities in .release-generations.json, captures the previous service
and external configuration in rollback-config/, and binds the matching private
state snapshot in rollback-data/. It builds a bounded inventory and delegates
all deletion to cleanup.py, inheriting the installation lock. Fresh acceptance
checks public Relay/Web identity, stable service identity, Wrapper connectivity,
snapshot integrity and live Codex configuration as the service user. Process,
symlink and dormant service/configuration dependencies override retention limits.
Linux discovery reads the installed systemd system/global/user load paths and
the actual UnitPath of running managers, including runtime and generated units.
Missing tools, unreachable managers or unreadable directories defer cleanup;
there is no fallback to a partial hard-coded directory list.
Environment-file discovery handles spaced assignments and continued lines,
preserves literal spaces in filenames, and retains dependencies even across
overrides/resets. Unresolved specifiers, wildcards or ambiguous quoting/escapes
defer cleanup instead of treating an optional file as absent.
Literal paths in service definitions, launchd plists and external environment
files are resolved component by component, including multi-hop aliases and
intermediate links owned by an old release. Those releases retain their runtime
dependency closure. Bare systemd Exec* filenames are resolved using the installed
systemd's systemd-path search-binaries-default, never the updater's shell PATH.
Discovery also retains a superset of literal ExecSearchPath and service PATH
overrides across definitions, drop-ins and environment files, following every
matching executable alias. Missing discovery tools, unresolved executables,
custom filesystem namespaces/inherited PATH, broken/cyclic links, unreadable
paths or ambiguous expressions defer cleanup. Supervisor commands still require
absolute paths. An absolute interpreter is not proof of a literal payload:
shell command strings, inline code, module lookup (including Python -m),
relative scripts and known command launchers such as env/nohup defer cleanup.
This guard also checks executable aliases and launchd ProgramArguments; it
never substitutes the updater's PATH for a service manager's inherited PATH.
Direct absolute script arguments remain literal dependencies, including the
installer's isolated Python wrapper_exec.py command. Hosts with unresolved
indirect commands need an explicitly reviewed retention inventory; a successful
update can therefore retain more than one previous generation. Discovery never
executes a service or recursively crawls arbitrary external directories.
The generated inventory also stores the service discovery context. Cleanup
rediscovers load paths, definitions, environment files and aliases before each
quarantine rename and permanent deletion, after any artifact tree walk. A newly
referenced candidate or incomplete scan stops the transaction and restores all
still-intact quarantined artifacts; regenerate the inventory before retrying.
Failed/incomplete acceptance or insufficient process visibility retains files and
warns without rolling back an already committed installation. A same-version
cc-remote update retries deferred retention with fresh checks, without restarting
services or sending model messages; --check is read-only. An interrupted
installation's prepared record requires explicit inspection, not automatic retry.
Unknown backups/uploads and external legacy roots remain outside automatic
deletion and still require the reviewed agent inventory below. Configuration
backup files.json records each original destination, absence, owner and mode;
keep it with that generation for administrator-controlled recovery.
v4.0.10 introduces automatic retention and Linux legacy migration. These take effect through its installer bundle, including upgrades initiated by older management CLIs. v4.0.9 and earlier do not provide these features. Source/manual deployments continue to use reviewed cleanup inventories and their existing activation transactions.
cleanup.py ships in both role bundles. It acquires the same .update.lock as
the installers, verifies the bound current link and completed transaction
records, and examines process cwd/executable/open-file references with lsof
plus argv references with ps. It also traces transitive symlink dependencies of
retained trees and live candidates, including shared venvs. Broken links,
unreadable paths or an exceeded scan bound stop cleanup. lsof is required;
missing tools, warnings and incomplete scans stop the
operation. User-owned private installations inspect that user's processes;
shared/system installations must run as root to cover every service user.
Create a mode-0600 JSON inventory outside the source repository, from the actual installation and its transaction records. All paths must be absolute, canonical, and owned/maintained within the operator's deployment scope. The tool does not discover unknown external transactions, infer generation age, or select files for you. List all relevant transaction records; if any outcome is unresolved, settle it first and retain its recovery set. Never list credentials, native sessions, live private state, project files or unrelated backups as candidates.
Inventory schema (replace the illustrative paths):
{
"schema": 1,
"installation_root": "/opt/cc-remote",
"current_release": "/opt/cc-remote/releases/release-new",
"rollback_paths": [
"/opt/cc-remote/releases/release-previous",
"/opt/cc-remote/rollback-data/before-new"
],
"protected_paths": [],
"service_context": {"home": "/home/cc-remote"},
"cleanup_roots": ["/opt/cc-remote/releases"],
"candidates": ["/opt/cc-remote/releases/release-old"],
"transactions": [
{"path": "/opt/cc-remote/transactions/activation.json", "field": "phase", "equals": "committed"}
],
"checks": [
{"name": "release and public health", "argv": ["/absolute/path/to/read-only-health-check"], "cwd": "/"}
]
}rollback_paths describes one complete recovery generation, including its
configuration/state snapshots; it must contain exactly one previous release.
Use protected_paths for additional service/runtime dependencies. Candidates
must be direct children of explicitly named, dedicated cleanup_roots; nested
candidates, symlink boundaries and mounts are rejected. Transaction expectations
accept only completed states (committed, complete, deployed_verified,
ready), and the records must remain unchanged throughout cleanup. Unknown
layouts remain retained until their provenance is established.
service_context enables repeated dormant-service discovery during cleanup.
Use the actual service HOME and, for Supervisor, include the registered
linux_service binding in that object. Managed installers always supply it;
older manual inventories without this optional field retain their static
protected_paths contract and must be refreshed/reviewed by the operator.
checks are operator-reviewed read-only argv arrays (no shell interpolation),
run before retirement, after quarantine, and after removal. They must cover the
actual role's release identity, stable services and health. For a Codex Wrapper,
also include a fresh configuration check, executed as the Wrapper service user:
<wrapper-python> deploy/check_codex_readiness.py \
--home <service-home> --release <active-release> --after <activation-epoch> \
--live-config --wait 0
# Add the installation's existing --plist or --env-file selector when needed.On a root-managed Linux cleanup, use runuser/sudo -u in that check's argv to
select the real service user. --live-config sends only initialize and
config/read to existing account sockets: it neither starts a daemon nor
creates/resumes a thread or sends a model message, and never prints configuration
contents. It catches a daemon that accepts connections but can no longer load
configuration. Verify an existing idle session's Wrapper status route separately
as part of final acceptance; do not send a model turn without authorization.
<python> deploy/cleanup.py /private/path/cleanup-inventory.json
<python> deploy/cleanup.py /private/path/cleanup-inventory.json --applyPreview does not run acceptance commands or remove artifacts. Apply rescans live references immediately before each rename and permanent deletion. Candidates are temporarily renamed beside their original path; a failed quarantine check restores the intact directory. A new live reference defers deletion and restores that path. Checks are observations, not a lock on external programs or configuration edits: operators must not launch jobs or repoint services at retired paths during cleanup.
The private <installation-root>/.cleanup-transaction.json records intent before
each mutation. Exit 0 means success (or preview), 2 means applied cleanup completed
with retained/deferred artifacts, and 1 means failure. A lost connection, partial
deletion, or failed final check requires inspection of this exact journal before
retrying. Do not remove the journal or rerun the command to hide an unknown result.
Quarantine is temporary transaction state, not another retained backup or Trash.
install.sh— versioned GitHub Release bootstrap. It requires an explicitrelayorwrapperrole, detects OS/CPU, downloads that one role archive, verifies itsSHA256SUMSentry before extraction, rejects unsafe archive paths, and then invokes the in-bundle installer. It never pipes a network response into a shell.cc-remote update— local management command registered by the role installers (scripts/cc-remote,cc_remote/update.py,install_cli.py). It discovers only registered installs carrying non-secretinstallation.jsonmetadata, downloads and validates one stable role bundle, then calls its existing role installer.--checkperforms no activation;--versionselects an exact published version. Both roles on one host require--role. Protocol changes require a coordinated maintenance window and--allow-protocol-change. SDK/service-contract changes defer to the Claude service migration procedure. The installer inherits the update lock so a disconnected caller cannot accidentally start a second update. The command must run outside the managed Wrapper/Relay process tree. Device updates first verify the paired Relay; an already-current compatible Relay is skipped. Otherwise--relay-sshselects an existing administrator's SSH target (saved in privateupdate.json). After verifying its managed domain, an OS-owned systemd job invokes the remote role installer. The private upstream transaction records that exact job before launch; an unknown outcome is inspected on retry, never blindly resubmitted. Public readiness precedes device activation. Pairing credentials do not authorize this operation. Other devices update individually. The new Wrapper role installer repeats upstream verification before stopping the local service, covering the first upgrade launched by v4.0.1's older updater.CC_REMOTE_RELAY_SSHsupplies an existing SSH admin target to that older caller; an interactive terminal can request it when missing. First pairing is separate from this existing-installation upgrade path. Explicit Mac registration can bind an existing immutable root and LaunchAgent; future installs preserve that layout and service identity. The command does not restart the independent Claude service, automatically adopt source/Docker layouts, or automate downgrade rollback. Recorded managed generations use the retention procedure above. Direct role installers use the same per-installation.update.lockbefore reading rollback state or changing services. They validate the inherited file descriptor from a managed update; an environment marker cannot bypass the lock.linux_service.py/wrapper_exec.py— Linux Supervisor adapter for the same Wrapper installer/update transaction. Explicitly bind the root, Unix control config, program/file, user, HOME and external environment/device files. Require root-owned non-replaceable code/config paths. Never infer arbitrary launchers: first adoption requires--adopt-supervisorand operator-reconciled environment selectors. Only replace/reload that program; preserve other sections and the independent Claude service. Preflight supports older Supervisor RPC responses (including 4.2.1/4.2.4) without the directory/uid fields added in 4.2.5. Always require the nativereloadConfigcomparison against active groups to report no changes for the bound program; a reread does not apply those changes. A failed fresh installation removes its newly loaded Supervisor group after stopping it and removing the new definition; previously configured programs remain registered during rollback. Snapshot selectors, Relay preflight, readiness and retention use that same binding. See the complete Supervisor installation/adoption procedure. Partial process visibility defers retention; it is not permission to delete container releases. Container recreation/persistence remains runtime-owned.build_release.py/release_manifest.py— reproducible role-bundle builder and fail-closed manifest validator. Relay artifacts containweb/distandrequirements-relay.lock; Wrapper artifacts contain no Web tree and userequirements-wrapper.lock. Each artifact carries the product version, protocol, full Git SHA, OS, architecture, and Python runtime contract.install-relay.sh— first-install/upgrade entry for a published Relay bundle. It creates secrets only when/opt/cc-remote/.envdoes not exist, then delegates to the existing transactional VPS installer. The explicit--allow-private-originsfirst-install option binds IPv40.0.0.0:8765for simultaneous LAN/Tailscale access and requires firewall restriction; the default remains loopback-only behind Caddy.install-wrapper.sh— first-install/upgrade entry for published macOS and Linux Wrapper bundles. It builds the immutable release before pairing and activation, stores device authority outside the release, atomically switchescurrent, installs a per-user LaunchAgent, root-managed systemd unit or explicitly selected Supervisor program, and restores the previous release/service definition on failure. The installer requires and explicitly selects the service user's daily~/.local/bin/claude; it never silently falls back to the SDK-bundled CLI. After activation,check_codex_readiness.pyreads a fresh, release- and process-bound result from Wrapper startup. Codex is optional: missing or incompatible CLI/account connections produce a separate warning instead of rolling back an otherwise healthy Claude/Wrapper installation. An existing Linux immutable system-service installation can explicitly migrate with--adopt-root /absolute/legacy/root --user USER.adopt_wrapper.pychecks its actual user, command, ownership, environment layout and absence of drop-ins before activation. The standard/opt/cc-remote-wrapperdestination must not already be installed. The old root is preserved; external credentials and service policy survive both migration and subsequent upgrades. A failure restores the old service and state rather than registering an incomplete destination. Unsupported layouts require explicit reconciliation, not a first-install overwrite. Root privileges are required; a narrowly authorized custom activation helper is not permission to bypass the system installer or its ownership checks.prepare_wrapper_stage.py— unprivileged preflight for an existing manual immutable-Wrapper topology. It reuses an active venv only when the dependency lock and Python pin are identical; otherwise it builds a platform-local venv with the pinned uv/Python, hashed binary wheels, and copy link mode. It validates imports plus the Python/Web protocol pair and writes a bound stage manifest for the separate privileged activation step. It never switchescurrentor restarts a service.setup-vps.sh— atomic VPS release installer. It validates a user-owned upload, copies it to a new root-owned/opt/cc-remote/releases/release-*directory, builds that release's own venv, validates the Python/web protocol pair, then switches the/opt/cc-remote/currentsymlink in one rename. The running tree is never overlaid withrsync --delete. If relay restart/readiness fails,current, Caddyfile, and the relay unit roll back together and the previous release is health-checked. The previous full code + web + venv directory is retained. Runsudo bash ~/cc-remote-upload/deploy/setup-vps.sh your-domain.com \ ~/cc-remote-upload; the optional second argument defaults to the repository containing the invoked script. Shared secrets stay only in/opt/cc-remote/.env, whoseWEB_STATIC_DIRmust point to/opt/cc-remote/current/web/dist.Caddyfile— reverse proxy + auto Let's Encrypt TLS (wss://domain/ws→127.0.0.1:8765) plus an early 4 KiB login-body limit. Replacecc-remote.example.comwith your domain. The application CSP allows HTTPS images from the exact GitHub hosts listed in the template, including the dedicated attachment redirect bucket, but not arbitrary external images, scripts, or fetch connections. The HTML preview runner remains isolated. Audio previews use bounded local Blob URLs permitted bymedia-src blob:. Image/media-policy changes require the managed Caddy configuration to be updated through the VPS activation transaction; replacing the Web bundle alone is insufficient. Do not replace the host allowlist withhttps:or wildcards.Caddyfile.insecure— explicit plain-HTTP public-IP template selected only whenALLOW_INSECURE_HTTP=1, the setup target is a public IPv4 address, andPUBLIC_ORIGINexactly matcheshttp://that-address. It omits HSTS and permitsws://in CSP; login credentials, cookies, wrapper tokens, and all session traffic are unencrypted in this mode. Pass the IP and source directory to the same immutablesetup-vps.shflow used for TLS.cc-remote-relay.service— systemd unit for the relay on the VPS.cc-remote-wrapper.service— systemd unit for the wrapper on your machine (editUser+ paths first). It reads root-only/etc/cc-remote/wrapper.env, hides that file and any legacy repository.envfrom model descendants, and disables core dumps.env.relay.example/env.wrapper.example— environment templates for each side. Install the wrapper template as root:root mode 0600 at the path above.com.muggle.cc-remote.wrapper.plist.in— secret-free macOS LaunchAgent template. The runtime reads the current user's mode-0600 device JSON instead of embedding control credentials in the plist.Dockerfile/docker-compose.yml/env.relay.docker.example— the same relay release as a container (buildweb/distin a Node stage, install the hash-locked wheels, run as theccremoteuser). See the container section below; this is an alternative to the systemd + Caddy path, not a fork of it.nginx-reverse-proxy.conf.example— a WebSocket reverse-proxy front for hosts that already run nginx instead of the managed Caddy. Loopback-only requirement is documented in the file header.work_registry_snapshot.py— snapshots provider-local Work SQLite databases through SQLite's backup API plus the complete private Claude/Codex profile migration transaction, restores matching pre-release data before an older wrapper is restarted, and verifies both engines' Work ownership backfills.
Protocol v74 is a coordinated upgrade: publish freshly built Relay/Web and
Wrapper artifacts from the same tagged commit. The strict protocol gate is
intentional and mixed protocol versions will not communicate. setup-vps.sh
rejects a missing or mismatched web build manifest. Stop the wrapper first;
activate the v74 relay/web release; then start the v74 wrapper.
The wrapper installer treats local Work data and versioned private control state
as part of the release
transaction. It stops the existing service, writes a private snapshot below
the install root's rollback-data/, starts the new release, and refuses the
activation unless the Claude and Codex Work schemas and all legacy profile
ownership rows are ready. On failure it stops the new process, restores both
SQLite images and the matching private profile state, then restores and starts
the previous code. If data restoration fails, it leaves the
wrapper stopped instead of running old code against a new schema. A manual or
legacy-layout deployment must use the same order: stop the wrapper, run
work_registry_snapshot.py snapshot from the new staging tree, activate and
verify v74, and retain that snapshot with the previous release. To roll back,
stop v74, run work_registry_snapshot.py restore, then switch and start the old
release. Never copy only registry.sqlite3 while the wrapper is live because
committed state may still be in its WAL file. Restoring a pre-release snapshot
also restores pre-release Work metadata: sessions, projects, or schedule state
created after activation will no longer be registered (their private files are
not deleted). Use this for immediate failed activation; after normal use,
prefer a roll-forward fix unless that metadata rollback is explicitly accepted.
Snapshot format v3 includes an explicit allowlist of Claude/Codex controls,
turn leases, pins, aliases, fork/BTW records, plans, presentation receipts,
Viewer associations, and both pending/completed profile journals. An absent
file is recorded too and removed on rollback if activation created it. Codex
checkpoint journals include their directory layout and local object data:
profile migration renames those directories, so manifest-only backup is not
sufficient. This private archive rejects symlinks and special files and is
bounded to 65,536 entries / 8 GiB of payload; exceeding a limit aborts before
activation, not with a partial usable snapshot. Restoring checkpoints retains
the displaced tree in .checkpoint-displaced-* under the private state
directory for recovery. Account configuration, credentials, native transcripts,
and project files are not part of this snapshot. Retain snapshots locally;
never publish them as release artifacts. Legacy v1/v2 snapshots remain
restorable only within their original, narrower scope; they cannot provide
complete rollback for a new profile migration.
For the optional static remote Viewer feature, also read
docs/remote-viewer.md. Default Bridge mode reuses
the existing origin, including explicitly allowed HTTP/IP access; no extra DNS/TLS
is needed. Include the runner asset and both Viewer WebSocket routes; preserve
the relay's HTTP sandbox/CSP on /__cc_viewer/bridge/* using the updated proxy
template. Home-directory page discovery is enabled by default; opt out with
CC_REMOTE_VIEWER_HOME_PREVIEW=0 in the Wrapper environment. It verifies explicit
HTML references or owned Python static listeners, not arbitrary URL proxies;
automatic pages stay in their session lists. Existing manual publications are
preserved. Check one actual page on each resource device, not just the catalog.
Optional Isolated mode
still requires wildcard TLS and a narrow frame-src addition. Never serve raw
Viewer scripts on the main application origin. Verify real mobile access before
reporting this optional feature as deployed.
The official relay install is a systemd venv staged by setup-vps.sh behind a
managed Caddy. Two alternative topologies are supported for hosts that already
manage their own services or TLS:
Docker container. Dockerfile builds the same relay release as a
multi-stage image: the Node stage compiles web/dist from source, the Python
stage installs the same hash-locked wheels setup-vps.sh pins and runs
python -m cc_remote.relay as a non-root ccremote user. From the deploy/
directory:
cp env.relay.docker.example env.relay # then fill in the secrets
docker compose up -d --build
curl https://your-domain/healthz # -> {"ok":true,...}The compose file publishes the relay only to the host loopback
(127.0.0.1:8765) and mounts a named volume for the SQLite device/Web Push
state. Public TLS + WebSocket termination stays with your existing front.
nginx instead of Caddy. nginx-reverse-proxy.conf.example terminates TLS
and proxies the /ws WebSocket to 127.0.0.1:8765. Keep it loopback-only:
the relay trusts forwarded transport metadata only from loopback peers.
Mainland-China mirrors. The Docker build defaults to PyPI.org. Behind the
GFW, build with Aliyun as the primary index and TUNA as the fallback (both
carry the sdist-only http-ece wheel):
docker build -f deploy/Dockerfile \
--build-arg PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/simple \
--build-arg PIP_EXTRA_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple \
-t cc-remote-relay .- Claude Code: run
claudedirectly for the untouched official process; Remote treats direct CLI, Desktop, and Agent View ownership as read-only. Explicit takeover may gracefully terminate the exact same-user Claude process with SIGTERM and then resume through the SDK, but it never kills the terminal shell, escalates to SIGKILL, or silently adopts a process. - Codex Code:
CC_REMOTE_CODEX_DAEMON=autorequires Codex's official shared app-server daemon. Set it tooffonly to force the legacy private stdio path. Optional multi-account installs provide either inlineCC_REMOTE_CODEX_PROFILES_JSONor a privateCC_REMOTE_CODEX_PROFILES_FILE; the macOS LaunchAgent defaults the latter to~/.cc-remote/codex-profiles.json. Each uniqueCODEX_HOMEowns a daemon. When a sibling home has no duplicate standalone payload, first bootstrap safely reuses the verified primary managed CLI through a profile-localcurrentlink; account data and daemon sockets remain isolated. Leaving both empty preserves the exact single-account path and UI. - Work: both engines stay on private per-process control planes regardless of the Code settings. Codex Work sessions and schedules may select any configured profile; the local registry freezes that ownership across retries and default-profile changes.
The required topology is CLI → the same official app-server ← Wrapper,
not merely two processes reading the same rollout. Check every enabled Code
account separately; never merge accounts into one CODEX_HOME to get sharing.
This does not apply to Work's deliberately private app-server.
-
Resolve the actual daily CLI (including shell aliases/launchers), the Wrapper's selected executable (
CODEX_BINif set), service user, and each account's effectiveCODEX_HOME. Compare real paths, not command names. A Codex CLI--profileis a configuration profile, not cc-remote's account home selection. Do not read or copy auth files. Both selected CLIs must supportapp-server daemonandapp-server proxy; an npm installation alone neither proves nor disproves that capability. -
Keep
CC_REMOTE_CODEX_DAEMON=autofor sharing; an explicitly configuredoffsurvives upgrades. Wrapper startup reuses each account's official daemon or invokes the native idempotentstartif it isn't reachable. It does not bootstrap, restart, replace a lagging daemon, or toggle Codex's separate cloud remote-control setting: those commands can interrupt native clients. Local TUI/proxy sharing uses the Unix listener without cloud remote control. Do not add a second daemon or another startup service. If sharing is unavailable, Code reports a connection error instead of silently starting a private stdio server. Explicitoffretains the legacy private path. Startup compares the CLI found on the service's PATH with Wrapper's selected binary, checks account socket and CLI/server versions, and initializes their official WebSocket proxy connections without creating a thread or model turn.Codex profile shared transport readyand the private rebuildablecodex-readiness.jsonreceipt describe this transport check. The Release installer checks its source release, fresh timestamp and live process identity before displaying the result. A failed or missing result never passes sharing acceptance, even if the Wrapper itself was installed successfully. This does not verify an operator's aliases, launch arguments or an already open TUI. Complete step 4 separately; do not relabel transport readiness as a verified ordinary terminal connection. -
As the same OS user, compare the following read-only probes using the resolved account home and both executable paths (replace placeholders):
CODEX_HOME="<account-home>" "<daily-codex-bin>" app-server daemon version CODEX_HOME="<account-home>" "<wrapper-codex-bin>" app-server daemon version
Require a running daemon, compatible CLI/app-server versions, and the same resolved
socketPath/managed server identity. For an already connected Code session, also check the Wrapper's actualapp-server proxy --sock …target and successful connection, not just that a socket file exists. With npm, Node and its native Codex child are one launch chain, not two independent clients. Do not expose complete process environments or user prompts in logs. -
Verify the operator's normal
codex resume <session-id>workflow actually connects to that same endpoint. Use an operator-approved idle test session or an already connected terminal; do not resume a busy production session for testing. Current Unix-socket connection evidence or structured app-server connection records tied to that CLI's lifetime can establish the route; old log rows, matching home paths andactive writererrors alone cannot. Confirm a shared session does not become read-only solely because its CLI is open. A live two-direction prompt test spends model tokens and requires explicit authorization; otherwise report transport verification separately from an untested live-message round trip.
The official CLI supports explicit endpoint selection with
codex resume --remote unix:// <session-id> for the selected home's default
socket, or --remote unix://<absolute-socket-path> for a specific endpoint.
See the official connection documentation.
This is a diagnostic/explicit connection option, not a mandatory suffix for
all resumes. If explicit connection works but plain resume does not, sharing
via automatic discovery has not passed acceptance: compare the actual CLI
build, home, endpoint and startup/connection errors. Do not assume all builds
auto-attach simply because a daemon is running, or mask the difference by
silently changing the user's shell alias.
The inspected official CLI 0.154.0 automatically probes its account's default
socket for ordinary launches. Additional launch configuration (for example
-c, a config profile, strict config or a custom exec-server) can select an
embedded server instead; a failed automatic connection can also fall back.
See the versioned native startup implementation.
Installers preserve these user choices rather than rewriting aliases or adding
--remote to every invocation. Version mismatches are reported so the operator
can finish active work before updating/restarting Codex.
An existing private CLI writer is not migrated into the daemon by starting it later. Let the operator finish and exit that CLI normally, then reconnect to the verified shared endpoint. Never kill an active CLI, delete locks/rollouts, disable ownership checks, or force takeover to make this check pass. Report any unverified account or stdio fallback as a remaining coordination issue, even when Relay/Web health is green; do not claim bidirectional deployment complete.
After core deployment and Codex CLI sharing checks, inspect each in-scope Wrapper desktop for an installed official Codex App. This is a post-deploy offer, not an installer side effect or a condition of Relay/Web health.
- macOS and Linux desktops have separate attachment paths. The
cc_remote.codex_desktophelper is macOS-only; Linux uses the account-scoped Linux launcher. Do not install an App on a headless server, crawl unrelated machines, or treat a PWA named cc-remote as Codex App. On macOS inspect bundle metadata (com.openai.codex); on Linux inspect the official package and desktop entry's actual executable. The Linux App may be named ChatGPT and includes Codex mode. - If no supported App is installed, skip the offer. If a previously approved shared entry is still verified for the selected account, preserve it without prompting again. A different account or changed setup needs a new choice.
- Otherwise ask, in the user's language, for example: “检测到本机装有 Codex App。 要让它与这个账号的 CLI、cc-remote 共用同一个会话服务吗?这会新增独立的 Codex Shared 启动入口,原 App 不改动;不接入也不影响 cc-remote。” Explain that the Desktop launch override is experimental and version-dependent. On multi-account hosts, confirm which account/home to use; do not silently select the first profile or change the default account.
- A decline or no answer means no App/launcher/configuration changes. Record
declinedorpending consentin the handoff, not a failed core deployment. Respect that choice on follow-up deploys unless the user changes it; do not invent a new tracking database solely to remember this offer. - An explicit request to attach the selected account already supplies consent; do not ask the same question again while carrying it out.
- After consent, follow the complete runbook for macOS or Linux, including preflight, installation, live transport checks, user-controlled quit/reopen and removal. Use that checkout's helper or documented launcher template; do not download unrelated scripts or patch the official App. Recheck the installed App build's launch transport rather than treating an internal environment variable as an official cross-version guarantee.
- App-control MCP tools are a separate opt-in. Describe that a prompt from CLI/cc-remote could then operate the desktop App, subject to native approvals. Only after that choice, follow the MCP guide. Inspect the App's bundled native plugin before adding an adapter. The custom macOS adapter does not implement Linux discovery.
Never force a running private App into sharing, kill a CLI/daemon, merge account
homes, modify the original App, relax signatures, or use global environment
overrides to pass this optional check. App attachment is not part of the
three-tier wire protocol activation and must not restart otherwise healthy
Wrapper/Relay services. Report core health, CLI sharing, App sharing and optional
tools separately; a visible launcher or queued UI action is not proof that a
panel opened or that full three-client messaging was tested.
The relay is exposed publicly; LOGIN_PASSWORD or LOGIN_USERS_JSON,
SESSION_SECRET, and WRAPPER_TOKEN or WRAPPER_TOKENS_JSON are the
authentication secrets. Claude defaults to
bypassPermissions; Codex inherits its local sandbox and defaults to approval
policy never. Treat every logged-in client as holding remote agent/shell
authority on the wrapper machine. Use strong secrets, keep relay .env out of
git, never store the production wrapper token in a model-readable repository
file. Always prefer TLS at Caddy; the public-IP escape hatch sends the login
password, browser cookie, wrapper token, and session traffic unencrypted.
See the security section of the main README.
The relay itself limits unfinished login bodies to 32 concurrent reads and 10
seconds each. The managed Caddy global block additionally sets 10-second header,
15-second body, 30-second write, 2-minute idle, and 64 KiB header limits before
requests reach the relay. Other global options and sites are preserved. If a
shared Caddyfile already contains an unmanaged servers block, setup fails
closed and asks the administrator to reconcile it instead of silently creating
ambiguous global behavior.