Everything the README leaves out: how it works, all install options, publishing, troubleshooting and development.
browser ──tailscale──> panel.py (BIND_HOST:8790, login cookie)
├─ /api/* tmux commands, state, GitHub (gh), limits, version
└─ /t/* raw proxy ──> ttyd (unix socket, user-only) ──> tmux attach -t cc-<name>
systemd --user (linger): cc-tmux (owns the tmux server: restarts of the panel never kill sessions)
cc-ttyd
cc-panel (KillMode=process)
The component diagram, the trust boundaries and the names in use are in ARCHITECTURE.md.
- tmux sessions are named
cc-<name>; options@cc_agent(a key ofAGENTSinpanel.py, such asclaude,codex,kimi,piorshell, or one of the agents inintegrations/agents.py),@cc_sid(the agent's conversation id) and@cc_skip(skip-permissions / bypass-approvals) live on the session. - Persistence: sessions are saved to
~/.config/cc-panel/sessions.jsonevery 5 s. If the tmux server PID changes (reboot, crash), missing sessions are recreated and agents resumed (claude --resume <id>,codex --no-daemon resume <id>). SessionStart hooks remember the top-level conversation; nested review agents cannot overwrite it. The same hook asks the top-level agent to show a finished task (screenshots, a link on the Tailscale address) unlessshow_resultsis off innetwork.json. Codex runs without a daemon so ancestry identifies its pane. Review and trust the newly registered Codex hook on its first launch; existing trust settings stay intact. Legacy sessions without a saved ID require choosing the conversation manually withcodex resume. Missing IDs/transcripts produce a clear error before stopping an agent; the panel never guesses the last conversation in a shared project. After a reboot such a session restores to a shell for recovery. - Files: code in
/opt/cc-panel, config in~/.config/cc-panel/(envholds the password), user units in~/.config/systemd/user/, install options in/etc/agent-deck/install.conf. - Out of memory (Linux): at every start the panel writes two drop-ins with
OOMPolicy=continue,~/.config/systemd/user/cc-tmux.service.d/agent-deck-oom.confand~/.config/systemd/user/tmux-spawn-.scope.d/agent-deck-oom.conf, so the kernel killing one process ends that process and not the tmux session. The second one matches the scopes of every tmux server of this user, not only DeckAway's. They stay after a downgrade or an uninstall; delete the two files and runsystemctl --user daemon-reloadto undo it. Nothing is written without a systemd user manager (Docker, macOS). - Caches, safe to delete, all under
~/.cache/agent-deck/and private to the user:gallery/<session>/(copies of the pictures a session showed: 200 pictures, 300 MB and 30 days at most),feature-groups/<repository>.json(commits sorted into features;git-groups/is the unused cache of versions before 1.28),summaries/<conversation>.<language>.json,thumbnails/. - Session strip:
/api/session_inforeads the tail of the agent's own conversation file for how full the context is and for a summary, written by a model of the session's own provider and never by another./api/galleryand/api/gallery_imageserve the kept pictures,/api/downloadany file under the home folder (40 MB at most, never~/.config/cc-panel),/api/git/groupsthe feature groups. - Feature groups: a background thread sorts new commits of the sessions' repositories and fetches their
originevery 15 minutes. The fetch uses the repository's own git config (SSH command, credential helper), as any git command in that folder does;ext::remotes are refused. - Interface updates: the page checks for a new UI every 30 s and when it returns from the background and reloads when idle (no draft or attachments); the mobile action menu has Обновить интерфейс, which keeps the draft and uploaded attachments.
- Attachments: up to four files, 200 MB each, including ZIP, text, PDFs and images. Codex receives supported images as native attachments and other files as local paths in its current conversation. Claude receives local paths. Files are not automatically extracted or executed. Uploads are temporary: files older than seven days are removed by a cleanup that runs every minute, or immediately when their session is deleted. The agent is told to copy anything needed long-term into the project.
- Hotkeys (desktop):
⌥1…9switch tabs,⌥↑/↓previous / next,⌥Tnew session.
install.sh (run directly or through get.sh / update.sh) reads these from the environment. They are
remembered in /etc/agent-deck/install.conf, so later runs keep them unless overridden.
| Variable | Default | Meaning |
|---|---|---|
DEV_USER |
dev |
Unprivileged user that owns tmux, the agents and the panel |
DEV_UID |
2600 |
UID for a new user (avoid 999/1000: often used by container processes) |
BIND_HOST |
Tailscale IPv4 | Address the panel listens on (never 0.0.0.0) |
PANEL_PORT |
8790 |
|
TS_AUTHKEY |
— | Join the tailnet non-interactively |
WITH_DOCKER |
1 |
Rootless Docker for the user (isolated from any root Docker) |
WITH_CODEX |
1 |
Codex CLI via its official installer |
MEM_MAX / CPU_QUOTA |
— | Hard caps for everything the user runs, e.g. 10G / 300% (no throttling threshold; lower CPU priority than the rest of the server) |
PUBLIC_DOMAIN |
— | Also publish at https://<domain> with Let's Encrypt |
PUBLIC_PROXY |
auto |
traefik (Dokploy present) or caddy (plain VPS) |
TRAEFIK_DYNAMIC |
/etc/dokploy/traefik/dynamic |
Traefik file-provider directory |
get.sh additionally honours AGENT_DECK_DIR (default /opt/agent-deck) and AGENT_DECK_REPO.
Manual system installation requires a root-owned checkout and protected parent directories:
sudo git clone https://github.com/matacoder/deckaway /opt/agent-deck, then
sudo /opt/agent-deck/install.sh. The bootstrap handles this automatically. Root entrypoints reject
user-owned, group/world-writable or symlinked source files. Files in the panel user's home and runtime
are written as that user, so a symlink cannot redirect a root write into a protected file.
The panel itself always listens only on the Tailscale IP; a reverse proxy on the same host terminates TLS.
- Dokploy / Traefik (
PUBLIC_PROXY=traefik):deploy/traefik-dokploy.ymlis rendered to/etc/dokploy/traefik/dynamic/cc-panel.yml. Traefik picks it up immediately, gets the certificate via HTTP-01 on the first request and renews it ~30 days before expiry. - Plain VPS / Caddy (
PUBLIC_PROXY=caddy): Caddy is installed from its official apt repository anddeploy/Caddyfile.cc-panelgoes to/etc/caddy/sites/cc-panel.caddy(imported from/etc/caddy/Caddyfile; the package's default site is replaced, other config is kept). The installer refuses to run if another process listens on 80/443 and opens them inufwif it is active.
Behind the proxy the panel takes the client IP from X-Forwarded-For (only from private-network proxies or
a proxy on the same host), so the login rate limit stays per client, and sets the cookie Secure on HTTPS.
If the certificate was requested before the DNS record existed, the log shows acme: error ... NXDOMAIN
and resolvers cache that answer for the zone's SOA minimum (dig SOA <zone>, often 5–15 min). Wait, then
retry:
- Traefik ignores edits that don't change the parsed config (a comment is not enough) and doesn't retry on
its own. Remove and re-add the file (no Traefik restart needed):
cd /etc/dokploy/traefik/dynamic && mv cc-panel.yml /tmp/ && sleep 3 && mv /tmp/cc-panel.yml . - Caddy retries by itself with backoff, or
systemctl reload caddy.
Check: docker logs dokploy-traefik 2>&1 | grep <domain> / journalctl -u caddy | grep <domain>, and
echo | openssl s_client -connect <domain>:443 -servername <domain> | openssl x509 -noout -issuer -enddate.
- The panel is a web terminal: whoever logs in gets a shell as
DEV_USER. Login form, signed HttpOnly cookie (90 days), 5 failed attempts per client → 1 min pause. ChangingPANEL_PASSWORDin~/.config/cc-panel/envlogs out every browser. DEV_USERhas no sudo and is not in thedockergroup; its Docker is rootless and cannot see root containers. UseMEM_MAX/CPU_QUOTAon shared machines.- ttyd listens on a unix socket in
/run/user/<uid>(mode 700), so other local users cannot reach it. ghdevice login grants access to all your repositories; for tighter scope log in with a fine-grained token:gh auth login --with-token.- Public access: consider a Traefik
ipAllowListmiddleware or SSO forward-auth in front of the panel.
The weekly limits come from the same places the agents use for /usage (Claude Code) and /status
(Codex): api.anthropic.com/api/oauth/usage and chatgpt.com/backend-api/wham/usage, called with the
agents' own tokens from ~/.claude/.credentials.json and ~/.codex/auth.json. These endpoints are not
documented and may change; the panel then shows "no data". Tokens are only read, never refreshed (the
agents rotate refresh tokens; a second refresher would log them out), and never sent anywhere else.
The limits line shows two numbers per subscription: what is left, and the plan, which is what would be left now if the limit were spent evenly until the reset.
- The panel checks the latest GitHub release every 6 h (
UPDATE_REPO=in~/.config/cc-panel/envdisables it). When a newer stable release exists, Update to vX.Y.Z appears in the sidebar and the mobile ⋯ menu. Click it to update the panel without root. - The detached updater downloads the official release from the configured GitHub repository,
validates its version, allowed files and Python syntax, backs up the installed panel and restarts
only
cc-panel.service. It restores the previous files if the updated login page fails its health check. Running tmux sessions and ttyd are left running. The UI displays progress or an error with a retry button; reloading the interface afterwards preserves drafts and attachments. - The button requires an installed, writable runtime directory (normally
/opt/cc-panel) owned by the panel user. It is disabled inside a git checkout. Downloaded install scripts are never executed; this updatespanel/only. System packages, proxy configuration, service units and agent binaries are updated separately through the installer. Unknown release layouts are rejected before stopping the running panel. A release must include the updater and the panel's required files. - A blank server still needs the initial
sudoinstallation to create the user, packages, services and HTTPS configuration. The panel itself and subsequent panel updates run as the unprivileged user. sudo /opt/agent-deck/update.shchecks out the latest tag (or passv0.2.0/main) and re-runsinstall.shwith the remembered options. It refuses to run with local changes in the checkout.- A local-model relay runs inside the panel process. Restarting the panel interrupts
active model requests; tmux persistence alone does not make that restart safe.
UI-only updates can replace
index.htmlatomically without restarting the service. Do not suspend an interactive agent with SIGSTOP/SIGCONT during deployment: its shell can reclaim the foreground terminal, and SIGCONT does not restore it. Wait for the agent to be idle before a backend restart; verify foreground terminal ownership as well as process liveness afterward. - Releasing (maintainers): add a
## X.Y.Zsection to CHANGELOG.md, then./release.sh X.Y.Z— bumpspanel/VERSION, tags, pushes and creates the GitHub release.
- Day-to-day changes need no root: as the panel user (e.g. from a terminal session in the panel) run
./deploy.shin your checkout. It copiespanel/to/opt/cc-panel, refreshes units /tmux.conf/ the Claude/Codex hooks and restarts the panel and ttyd; the tmux server and sessions keep running. - Root (
sudo /opt/agent-deck/install.sh) is only needed for system-level changes: packages, users, Traefik / Caddy. python3 panel/make_icons.pyre-renders the app icons.
sudo -u dev tmux ls # sessions
sudo -u dev tmux attach -t cc-<name> # attach over SSH
sudo -u dev XDG_RUNTIME_DIR=/run/user/$(id -u dev) systemctl --user status cc-panel cc-ttyd cc-tmux
sudo journalctl _UID=$(id -u dev) -f # logs
grep PANEL_PASSWORD ~dev/.config/cc-panel/env # panel passwordChoose Pi in the new-session dialog, then an LM Studio profile and model.
Pi uses the panel's authenticated loopback relay and the same local generation
metrics as Claude sessions. No Pi cloud account is required. Install/update uses
npm install --global --prefix "$HOME/.local" @earendil-works/pi-coding-agent@1.0.2
and requires Node.js 22.19.0 or newer.
Each model binding has private Pi configuration and transcripts under
~/.config/cc-panel/integrations/pi/<binding>/. Relay credentials are injected
through the child environment rather than stored in models.json or command
arguments. Startup catalog refresh is disabled. Resume opens the exact saved
conversation for the same project; it fails if that transcript is missing.
The panel backend must be updated to enable the Pi selector. An active local
model request must finish before restarting the backend, which owns the relay.
Edit frontend/index.html, frontend/style.css and the scripts in frontend/, then run
python3 scripts/build-panel.py. The scripts share one scope and are joined in the order of SCRIPTS in
that file, app.js last; the build fails when a file is missing from the list or a top-level name is
declared in two files. Commit the generated panel/index.html together
with the sources. CI checks that the bundle is current. Releases retain a single
HTML file so installed updaters can validate and install them without new asset
allowlists or separate browser cache invalidation.
Project-directory settings are stored in ~/.config/cc-panel/projects.json.
The panel reads them on startup and applies UI changes immediately for new
sessions. Explicit installer --projects-dir/PROJECTS_DIR values override the
saved preference; otherwise macOS reinstalls retain it. Session labels are stored
in tmux @cc_title and restored from session state; internal IDs remain stable.
integrations/decks.py owns profiles in the private
~/.config/cc-panel/integrations/decks.json file (0600). Remote authentication
cookies remain in memory; passwords and cookies never enter browser responses.
Discovery probes only online Tailscale peers from tailscale status --json.
Destinations must be numeric IPv4 addresses in 100.64.0.0/10; DNS targets,
non-tailnet addresses, credentials in URLs and redirects are rejected.
The gateway authenticates every /deck/<id>/... request and checks same-origin
POSTs and WebSocket upgrades. It forwards only API and terminal paths, translates
the remote Origin and sends its own remote cookie rather than the browser's
gateway cookie. A remote 401 permits one authentication refresh; transport
failures never automatically replay a mutation. Terminal handshake responses
strip remote cookies before bridging bytes. Local /t/ still uses the ttyd Unix
socket. Browser storage keys are scoped by instance while local keys remain
compatible. UI revision checks continue to use the gateway's own revision.
~/.config/cc-panel/network.json stores the gateway label and advertised public
URL. The effective listener is configured by BIND_HOST/BIND_PORT; DNS, HTTPS
and reverse proxies remain installer/server settings.