Repository navigation
Releases
v1.3.2
v1.3.1
Changed
- The embedded web client shows agent messages, pasted text and background-task notices as readable chat rows instead of raw harness tags (see the frontend changelog).
v1.3.0
Added
scripts/deploy.sh: builds the image from the checkout with the git version and restarts it, then checks/healthz.
Fixed
- Images built locally with
docker composereport their version instead ofdev(BRIDGE_BUILD_VERSION). scripts/build.shtags the imageghcr.io/spacecodee/spacee-herdr-bridge, andrelease.shbuilds with the release version instead of a-dirtygit describe.- The chat shows only the live branch after a rewind: Claude Code (
/rewind), pi (/tree), Codex (backtrack) and opencode (/undo,/redo) no longer render abandoned messages, in the conversation view and in the home summaries. - Messages pulled back from the Claude Code queue with Up no longer stay in the chat as queued.
- After a Codex rewind the chat keeps the earlier turns: the new rollout file continues the old one (
history_base), and the bridge reads both.
1.2.1
Added
- Prebuilt multi-arch image (
linux/amd64,linux/arm64) onghcr.io/spacecodee/spacee-herdr-bridge, with OCI labels;docker compose pull && docker compose up -druns it,BRIDGE_VERSIONpins a release.release.sh --push-imagebuilds from the release tag in a temporary worktree and always logs out ofghcr.ioafterwards. - MIT
LICENSE,SECURITY.md(threat model, how to report a vulnerability),CONTRIBUTING.md, a README rewritten as the public landing page, and a host setup guide with placeholders instead of machine-specific values.scripts/herdr-core-restartmessages are now in English. - Local release scripts (no CI):
scripts/build.shbuilds the frontend, embeds it, runs the checks, and produceslinux/amd64andlinux/arm64binaries withSHA256SUMSinbin/release/plus a local Docker image;scripts/release.sh <version>updates both changelogs, tags both repos and never pushes;scripts/release.sh --push-image <version>pushes a multi-arch image toghcr.io. The bridge has a-versionflag.
Changed
- The Dockerfile cross-compiles with Go instead of emulating the target platform;
compose.yamlnames the GHCR image and still builds locally with--build.
The web app changes for this version are in spacee-herdr-frontend v1.2.1; its build is embedded in this binary.
1.2.0
Added
GET /api/chat/summariesitems gain an optionalasking(count, firstquestion) while the newest agent question is still pending; absent once answered. Never carries options, answers or secrets.- The chat stream marks agent questions for Claude Code (
AskUserQuestion), pi (theask_user_questionandask_user_choicetools) and Codex (request_user_input, Plan mode) and opencode (question): the tool part carriesquestions(question, header,multiSelect,otherwhen a typed answer is allowed,secretfor Codex secret questions whose answer is masked, options (empty for a free-text question) with label and description; at most 4 questions and 6 options, text clipped) and staysrunningwhile pending; once answered it becomesokand carriesanswers(question text to chosen answer, multiple choices joined with ", "). A reconnectresetshows unanswered questions as pending.
The web app changes for this version are in spacee-herdr-frontend v1.2.0; its build is embedded in this binary.
1.1.0
Added
- Per-device sessions: each login creates a device (id, name, platform from the User-Agent) kept in
devices.jsonbeside the token (mode 0600, at most 20; signing in on one more signs out the least recently seen).GET /api/deviceslists them (currentmarks the caller),PATCH /api/devices/{id}renames,DELETE /api/devices/{id}andDELETE /api/sessionsign a device out: its push subscriptions are removed and its open/wsand/api/chatstreams close within a second. - Pairing codes: a signed-in device gets a 6-character code with
POST /api/devices/pair(valid 5 minutes, single use, at most 5 live) and a new device logs in with{"code": "..."}onPOST /api/session. Ten wrong codes in ten minutes invalidate all codes and block code logins for ten minutes (token login keeps working). - Push subscriptions record the device that saved them (
deviceIdinpush.json; old entries stay valid). - An agent is recognized before its first message:
/api/chatsends an emptyresetwith the agent name (instead ofnone/not-found) and/api/chat/summarieslists the terminal withmessages: 0, so the app shows its icon and opens the chat right away (Claude Code, pi, Codex, opencode). - Chat transcripts show Claude Code messages typed while the agent was working: a message waiting in Claude's queue is sent with
queued: trueand removed through a newremoveSSE event ({"ids": [...]}) once Claude takes it; a queued message delivered in the middle of a turn is now kept as a user message (it used to be missing from the chat). Background task notifications in the queue are not shown.
Changed
- Host setup guide: a systemd user unit for herdr-core (
scripts/herdr-core.service) and a clean restart script (scripts/herdr-core-restart), signing out a lost device instead of rotating the token, and what lives in~/.config/spacee-herdr. - The session cookie no longer holds the access token: it is
<device id>.<device secret>and only the secret's hash is stored. Cookies from before (equal to the token) are upgraded to a device automatically on first use, so signed-in phones stay signed in.
Fixed
- Chat transcripts no longer show the raw markup of Claude Code slash commands:
/clearor/model opusappear as typed, and a command's output (for example "Set model to …") appears as a system message (empty output is dropped).
The web app changes for this version are in spacee-herdr-frontend v1.1.0; its build is embedded in this binary.
1.0.0
Added
-
Token login: the access token is created on first start in
~/.config/spacee-herdr/token(mode 0600) and exchanged for an HttpOnly, Secure, SameSite=Strict session cookie. -
Authenticated WebSocket
/wsthat relays bytes between the browser and the local herdr-core socket; upgrades from a foreign Origin are rejected. -
The web client is embedded in the binary and served with single-page-app fallback.
-
/healthzendpoint for local checks. -
Docker image and Compose file: runs as the host user, publishes only on host loopback (
127.0.0.1:8080), mounts the core socket directory and the token directory, restarts with Docker. -
Host setup guide (
docs/host-setup.md): tailnet settings, tailnet policy with tests that keep tagged machines (the VPS) away from the host,tailscale serve(tailnet only, no Funnel), iPhone login, verification table, token rotation, migration to a new machine, troubleshooting. -
Web Push: the bridge watches core's
events.subscribestream and sends a push (Angular service worker format, TTL 1 h) when a terminal's agent becomesblocked("Needs you", high urgency) ordone, or a terminal exits or loses its connection. Only transitions notify (never snapshots or reconnects), at most one per terminal per 10 s, and no terminal output is ever included. VAPID keys are created on first start in~/.config/spacee-herdr/vapid.jsonand subscriptions kept inpush.json(both 0600, at most 10 browsers; an unreadablepush.jsonis renamed topush.json.corrupt-<unix-seconds>and the bridge starts with no subscriptions, an unreadablevapid.jsonstops it). New authenticated endpointsGET /api/push/key,POST/DELETE /api/push/subscriptionandPOST /api/push/test; new flag-push-subject. Usesgithub.com/SherClockHolmes/webpush-gofor VAPID signing and RFC 8291 encryption. -
Image upload: authenticated
POST /api/upload(same-origin only) takes a raw PNG, JPEG, WebP or GIF (type checked from the bytes, at most 15 MiB) and stores it on the host as<YYYYMMDD-HHMMSS>-<random>.<ext>(file 0600, directory 0700), returning{"path": "<absolute host path>"}so a terminal agent can read the image. Files older than 7 days are deleted at startup and hourly. New flag-upload-dir(default$XDG_CACHE_HOME/spacee-herdr/uploads, else~/.cache/spacee-herdr/uploads); Compose mounts that directory at the same path inside the container. -
Chat transcripts: authenticated, same-origin
GET /api/chat?terminal=<address>streams the normalized, read-only transcript of the agent running in a terminal as Server-Sent Events (resetwith the last 200 messages, thenappend/update,nonewith a reason, 15 s heartbeat; the file is polled every 500 ms and only appended bytes are read). Adapters for Claude Code (~/.claude), pi (~/.pi) and Codex (~/.codex); the agent is found by walking the terminal's foreground process tree in/proc. Text parts are cut at 64 KiB and tool input/output at 8 KiB, images are never sent as base64, paths and messages are never logged, at most 8 streams (429beyond). New flags-homeand-proc. -
Chat summaries: authenticated, same-origin
GET /api/chat/summariesreturns one entry per live local terminal whose agent has a transcript (terminal,agent,session,title= first user prompt without slash commands (80 chars),last= plain-text preview of the newest message or tool call (140 chars) with its role and time,messages) for the conversations list. Only the last 256 KiB of each transcript is read (the first 64 KiB too when the tail has no user prompt), somessagescounts that window, and results are cached per file until its size or mtime changes. Shells and unsupported agents are omitted; contents and paths are never logged. -
Chat adapter for opencode: sessions are read straight from its SQLite database (
~/.local/share/opencode/opencode.db), opened read-only (mode=ro,query_only, 2 s busy timeout; never written or checkpointed, WAL rows from a running opencode are visible) through the pure-Gomodernc.org/sqlitedriver (no CGO; the binary grows about 6 MB). The session is the newest top-levelsession_v2row whosedirectoryis the terminal's cwd (same caveat as pi and Codex); messages come fromsession_messagebyseq(text, reasoning, tool calls with status/input/output, attachments as notes), polled every 500 ms for new rows and for rows updated in place (a running tool completing). Works for/api/chatand/api/chat/summaries. -
Layout management through herdr-core, with the desktop window closed: authenticated, same-origin JSON endpoints (bodies up to 8 KiB) edit the
herdr-appdocument and start or kill the terminals behind it.POST /api/layout/space(name, absolutefolder, optionalmachinefrommachine.list, optionalargv),POST /api/layout/tab,POST /api/layout/split(right/down, next topaneor the last pane),PATCH /api/layout/space/{id}and/tab/{id}(rename),POST /api/layout/moveandDELETE /api/layout/{space,tab,pane}/{id}?confirmed=1(withoutconfirmed=1they answer 400; deleting also kills the terminals; a tab keeps its last pane and a space its last tab,409).argvis limited to a shell (none) or exactlyclaude,pi,codex,opencodeoragy. New terminals carry theherdr.space/herdr.tablabels and are started before the document is written (a failed write kills them, no orphans); writes use the document version and retry up to 3 times from a fresh read on conflict. Unknown document fields are preserved. Only operation names and outcomes are logged. -
Compose:
pid: host(to see the agents' processes) and read-only mounts of~/.claude,~/.pi,~/.codexand~/.local/share/opencodeat the same paths; the security impact is explained indocs/host-setup.md.
Changed
- The bridge runs as a Docker Compose service instead of a systemd user service; update with
docker compose up -d --build.
Fixed
- PWA serving: a missing file (
/ngsw.json,/nope.js) now returns 404 instead ofindex.html; only extensionless app routes fall back to the SPA..webmanifestis served asapplication/manifest+json, and cache headers are set (no-cacheforindex.html, the service worker and the manifest; immutable for hashed build files; one day for icons and fonts) so updates reach installed apps.