Files
ai-agent/sidecar/README.md
Gabriel Vidal 51effbc74f feat(accounts): per-account Claude launches in the sidecar + hard-coded account profiles
The sidecar launches a claude run on one Claude login: the default account
(the CLI's own ~/.claude) or one declared in SIDECAR_ACCOUNTS (work=~/.claude-work),
exported as CLAUDE_CONFIG_DIR for that run. account on /spawn, /resume and /fork;
fork finds its source in that account's projects/; a non-default account with no
login is a 409; ANTHROPIC_API_KEY is stripped once accounts are declared; no
--remote-control for non-default accounts; GET /accounts = claude auth status.

backend/accounts.py holds the personal/work profiles and the resolution order
(explicit stamp -> import source -> cwd rule -> default), with unit tests.
2026-09-11 19:38:54 +02:00

98 lines
4.3 KiB
Markdown

# claude-sidecar
A tiny **host-side** FastAPI wrapper that launches `claude -p` sessions on behalf
of the [ai-agent](../) viewer.
## Why a host process (not a container)
The ai-agent viewer runs in Docker and can't reach the host's `claude` CLI — its
auth (`~/.claude/.credentials.json`), RTK/vault hooks, and skills all live in the
host user's home. This sidecar runs natively on the host **as the repo owner**, so
a session it spawns is exactly like one started from a terminal.
## Flow
```
Frontend (sticky input)
│ POST /api/spawn {prompt}
▼
ai-agent backend (container)
│ generates a session UUID, POST /spawn to the sidecar
▼ http://host.docker.internal:8790/spawn (Bearer SIDECAR_TOKEN)
claude-sidecar (host)
└─ claude -p <prompt> --session-id <uuid> --output-format json \
--permission-mode bypassPermissions --model opus \
--remote-control (detached)
```
`--remote-control` is added by default so every spawned run registers with
Claude Code Remote Control and shows up in the Claude app — you can watch and
drive the conversation from your phone. Set `SIDECAR_REMOTE_CONTROL=0` to opt
out.
Claude writes its transcript to
`~/.claude/projects/-home-gabrielvidal-homelab/<uuid>.jsonl`, which the ai-agent
container already watches read-only. The new conversation appears in the viewer
within ~2s and the frontend redirects to it once it has synced.
The conversation view has a matching sticky composer that continues an existing
thread: it POSTs `/api/spawn`'s sibling `/api/resume`, which the backend proxies
to `POST /resume` here — `claude -p <prompt> --resume <sessionId>` in the
session's original cwd. Because resume reuses the session id, the new turns
append to the same transcript and stream straight into the open conversation.
## Install (host)
```bash
sidecar/install.sh
```
Creates a venv, writes a systemd **user** unit (`~/.config/systemd/user/claude-sidecar.service`),
and starts it. Reboot survival needs `sudo loginctl enable-linger $USER`.
Config comes from the repo `.env`: `SIDECAR_TOKEN`, `SIDECAR_PORT` (default 8790).
The ai-agent container reads `SIDECAR_URL` + `SIDECAR_TOKEN` (see its
`docker-compose.yml`, which also adds `host.docker.internal:host-gateway`).
## Endpoints
- `GET /health` → `{ok, cwd, claude, accounts, defaultAccount}`
- `GET /accounts` (Bearer auth) → each account's `claude auth status`
(`loggedIn`, `email`, `orgName`, `subscriptionType`), cached 60s.
- `POST /spawn` (Bearer auth) `{prompt, sessionId?, model?, cwd?, account?}` →
`{sessionId, pid, log}` — spawns detached, returns immediately.
- `POST /resume` (Bearer auth) `{sessionId, prompt, model?, cwd?, account?}` →
`{sessionId, pid, log}` — continues an existing conversation by running
`claude -p <prompt> --resume <sessionId>`. Reuses the original session id, so
the new turns append to the same transcript and stream into the open
conversation. `--resume` is directory-scoped, so pass the session's original
`cwd`.
- `POST /interrupt` (Bearer auth) `{sessionId}` → `{sessionId, pid, signal, ok}`
— sends the run's process group a `SIGINT` (like Ctrl+C), so Claude aborts the
turn, writes a `[Request interrupted by user]` marker and exits. `404` if no
live process is tracked for that session.
Per-session stdout/stderr is captured under `logs/<sessionId>.log`; the pid is
recorded in `logs/<sessionId>.pid` so `/interrupt` can find the run.
## Accounts (personal vs work)
A claude run launches on one Claude login. The default account is the CLI's own
config (`~/.claude`); `SIDECAR_ACCOUNTS=work=~/.claude-work` (in the unit, written
by `install.sh`) declares more, each exported per run as `CLAUDE_CONFIG_DIR`.
`account` on `/spawn`, `/resume` and `/fork` picks one — resume and fork must pass
the account the session *started* on, since its transcript lives only in that
account's `projects/`. A non-default account whose dir has no `.credentials.json`
answers **409** (never a fallback onto the default login), `ANTHROPIC_API_KEY` is
stripped from every run once accounts are declared, and `--remote-control` is only
added for the default account. The account is recorded in the pidfile and listed
by `/sessions`.
## Manage
```bash
systemctl --user status claude-sidecar
systemctl --user restart claude-sidecar
journalctl --user -u claude-sidecar -f
```