README
¶
Warren Headless
Warren Headless holds Projects, Workspaces, Git worktrees, and Terminal Sessions on a Host (local Mac or remote VPS). Ghostline is the sole terminal runtime. Both the Desktop and the CLI are clients; a client disconnecting never ends a Session.
Installation
A remote host needs Go 1.25 and Git. The default runtime is ghostline: server-side PTY sessions with libghostty-vt snapshots, needing no terminal multiplexer. Configurations selecting any removed runtime are rejected at startup; use ghostline.
go install github.com/abcdlsj/warren/Headless/cmd/warren-headless@latest
go install github.com/abcdlsj/warren/Headless/cmd/warren@latest
Or build the current revision from the repository:
mise run build:headless
On macOS, the headless binaries are built for arm64 and require macOS 13 or
later on an Apple Silicon Mac. Ghostline v1 statically links its terminal core.
The app package also includes the v0.8 compatibility binary and its arm64
libghostty-vt.dylib for a one-time v0 -> v1 migration, with no separate
Ghostty checkout.
warren-headless listens on 0.0.0.0:8789 by default so phones and tablets on the same LAN can open the Web UI directly. It also serves the same UI over HTTPS on 0.0.0.0:8788 (see "LAN HTTPS" below). The HTTP port has no TLS, so do not expose it to the public internet.
Start and Connect
Start it directly on the remote host:
warren-headless
Default files:
- State:
~/.warren/state.json - Token:
~/.warren/token - ghostline socket:
~/.warren/ghostline.sock(default runtime) - Worktrees:
~/.warren/worktrees/
From your Mac, warren ssh starts the remote daemon, fetches the token, saves the endpoint, and sets up port forwarding:
warren ssh user@vps
Keep that process running. The Desktop reads the endpoint from ~/.warren/config.json and shows Local plus server options in the top-right corner. Endpoint changes made with warren endpoint add|use|remove are picked up within about a second, so restarting Warren is not required.
LAN HTTPS
To remove the browser's "Not secure" badge when opening the Web UI from a phone on the same LAN, use the HTTPS listener on 0.0.0.0:8788 (disable it with --lan-https= or an empty WARREN_LAN_HTTPS). On first start the daemon generates a local CA and a server certificate in ~/.warren/tls/; the certificate includes localhost and the machine's current LAN IPs.
- Open
http://<host-LAN-IP>:8789/tls/ca.pemon the phone, install the CA certificate, and enable full trust for it (iOS: Settings → General → VPN & Device Management, then Certificate Trust Settings; Android: Settings → Security → Install certificates). - Open
https://<host-LAN-IP>:8788/#t=<token>.
The loopback HTTP endpoint 127.0.0.1:8789 keeps serving Desktop and CLI clients unchanged. If the machine's LAN IP changes, restart the daemon so the server certificate is regenerated with the new IP.
CLI
warren endpoint list
warren --endpoint my-vps project add /srv/git/my-project
warren --endpoint my-vps project add /srv/git/my-project --auto-import-worktrees
warren --endpoint my-vps project list
warren --endpoint my-vps project move PROJECT_ID --before OTHER_PROJECT_ID
warren --endpoint my-vps workspace create PROJECT_ID --branch release/my-feature
warren --endpoint my-vps workspace move WORKSPACE_ID --before OTHER_WORKSPACE_ID
warren --endpoint my-vps agent create WORKSPACE_ID --provider codex --prompt "Run the relevant tests"
warren --endpoint my-vps agent create WORKSPACE_ID --provider codex --command codex-alias --no-prompt
warren --endpoint my-vps agent create WORKSPACE_ID --provider opencode --prompt "Run the relevant tests"
warren --endpoint my-vps agent create WORKSPACE_ID --provider opencode --command opencode --no-prompt
warren --endpoint my-vps session move SESSION_ID --group GROUP_ID --confirm
warren --endpoint my-vps session move SESSION_ID --workspace WORKSPACE_ID --confirm
warren --endpoint my-vps session current
warren --endpoint my-vps session move --current --workspace WORKSPACE_ID --dry-run
warren --endpoint my-vps session move --current --workspace WORKSPACE_ID
warren --endpoint my-vps session list
warren --endpoint my-vps session attach --current
warren agent list
warren agent read AGENT_ID --text-only
warren agent send AGENT_ID "Run the relevant tests" --wait --timeout 30m
warren agent wait AGENT_ID --timeout 30m
warren agent attach AGENT_ID
warren session send SESSION_ID "Run a shell command"
warren session read SESSION_ID --timeout 8s
All commands support --json. worktree is an alias for workspace; help
and error messages keep the command name you typed instead of rewriting it to
the canonical name. warren help, warren --help, and
warren <command> --help print help and exit 0. Missing or invalid arguments
print the relevant usage and exit 2; server errors use exit code 1.
Roster-heavy list commands return at most 10 rows by default so an Agent
does not receive an entire long-lived roster in one context window. Use
--all when the complete result is required, and prefer searching that
explicit full output with rg, for example:
warren session list --all | rg 'codex|workspace-id'
warren workspace list --all | rg 'release/'
Commands default to aligned, human-readable tables. Pass --json for stable,
machine-readable JSON output. workspace create reports created and
gitWorktree in its result, so scripts know whether a Git worktree was really
created and where it landed. session list shows running sessions by default;
the default output is limited to 10 rows. Pass --all for the complete list
(including ended sessions), or --ended to list only ended ones. Use
--limit N for a smaller bounded result.
Each session row exposes the Warren Session ID separately from the
agent/thread ID and transcript path. JSON rows also include current: true
when the row's Warren Session ID exactly matches WARREN_SESSION_ID; no cwd,
name, timestamp, or transcript inference is performed.
project add --auto-import-worktrees stores automatic Git worktree import on
that Project and imports every currently existing external checkout without a
confirmation prompt. The Web and Desktop project context menus also expose
this toggle. Their Import Existing Worktrees… action opens a one-time
multi-select list; already registered worktrees remain visible but disabled.
warren agent create is the primary Codex/Claude/OpenCode entry point.
--provider selects the transcript protocol and --command selects the
executable or alias (defaulting to the provider name). For Codex and Claude,
--prompt is appended as the provider's initial positional prompt. OpenCode
receives the initial text through its --prompt option. --command may contain
executable options but must not contain its own positional prompt or prompt
option. Warren also rejects provider print/non-interactive mode because
agent send relies on the interactive composer. Use --no-prompt to create an
idle Agent explicitly. --wait can wait for that first turn to finish.
OpenCode sessions are deliberately started without --continue, --session,
or --fork. Warren binds one OpenCode conversation to one Warren session; a
resume or fork flag would make the provider identity diverge from the durable
Warren identity. With --no-prompt, OpenCode does not create its SQLite
conversation until the first prompt is entered in Terminal; after that initial
input, send subsequent prompts through the same Warren Agent session. With
--prompt, Warren performs that first turn during creation.
warren agent read AGENT_ID reads the normalized transcript, never the PTY.
By default it returns the newest 20 user, assistant, and error activities and
limits text fields to 2,000 characters. --tools adds compact tool-call
records; --tool-output is the explicit opt-in for bounded raw tool results.
Use --recent N, --all, --include TYPE,..., --filter TYPE,..., or
--text-only to control the normalized projection. --full is the rare
escape hatch: it streams the exact JSONL from the transcript bound to that
Agent, without loading the whole file into the daemon or CLI. agent send
waits for the transcript watcher, writes the composer text, and submits a
separate kitty-protocol Enter event. agent wait blocks until the current or
next turn finishes. agent attach is the explicit raw TTY operation.
session is the generic PTY resource. session create starts shells,
custom commands, and other interactive programs; it does not create Codex,
Claude, or OpenCode Agents. session send writes terminal input and session read returns
raw PTY output with --timeout/--contains. Agent transcript and turn flags
are rejected on these commands so a TUI cannot be mistaken for conversation
data. The trae preset likewise only launches a shell command; it is not an
Agent provider and has no transcript, activity, or Agent CLI semantics.
Agent and Session commands use Warren IDs. agentThreadId remains a separate
provider conversation ID in roster output. Use --current when
WARREN_SESSION_ID identifies the target resource.
For workspace create, --branch is required and --path is optional: omit
--path and the daemon places the new worktree under
~/.warren/worktrees/<project>/<workspace>-<branch>; pass --path /custom/path
only when the worktree must live somewhere specific. session create starts a
durable terminal in an existing workspace; the Desktop and Web clients see it
as soon as the daemon broadcasts the updated roster. Pass --title NAME to
set the user-facing name shown in tabs; session rename SESSION_ID --title NAME
changes it later. Without a user-set name, clients fall back to a generated
default (kind or command), and a user-set name always takes precedence.
session move SESSION_ID --workspace WORKSPACE_ID moves a standalone Terminal
Group session into a Workspace; session move SESSION_ID --group GROUP_ID
moves it back. The running process, cwd, output history, and Session ID are
preserved, so the tab simply appears under the destination context. Use
session current or --current from a Warren-managed shell to target only the
session named by WARREN_SESSION_ID. --current refuses to guess when the
binding is missing. --dry-run (also --preflight) returns the exact source,
destination, agent binding, and transcript path without changing state.
Explicit-ID moves require --confirm (or --yes) unless an expected source
context is supplied with --expected-workspace or --expected-agent-session;
this keeps a copied but valid ID from being treated as sufficient intent.
Current-session moves automatically send the observed workspace and agent
session IDs as compare-and-swap expectations; a stale observation fails with a
refresh-and-retry error instead of moving a changed session. Successful moves
return an operation ID, and session undo OPERATION_ID reverts only when the
session still has the recorded post-move context. Deletes expose the same
target information through session remove ... --dry-run; deletion has no
automatic undo because its runtime and transcript side effects are not safely
reconstructible.
On macOS, mise run install also initializes a local endpoint pointing at
http://127.0.0.1:8789 with the daemon token from ~/.warren/token, so the
CLI works against the local daemon without extra setup. On a remote host, use
warren ssh user@vps to create and select the endpoint instead.
API Boundaries
The control interface is /v1/ws: authenticate with the token first, then use request/response messages with request IDs. Roster is the Host resource projection; terminal output uses WebSocket binary frames. project.move and workspace.move persist the sidebar order on the Host (both accept id and an optional before; omitting before moves the entry to the end). session.current accepts only an already-resolved Warren Session ID, while session.move.preflight and session.delete.preflight validate context without mutation. session.move accepts optional expectedWorkspace and expectedAgentSession guards and returns a mutation operation ID; session.undo is compare-and-swap guarded. session.attach subscribes to output only. The client that owns UI focus sends session.focus with optional cols/rows to control the shared terminal size, while background session.resize requests are safe no-ops. SSH, Tailscale, and future Relay provide reachability only and do not enter the resource domain model.
Public Access tunnel lifetimes are bound to the daemon: every running adapter is stopped on shutdown, and a gnar process left behind by a crashed daemon is reaped before the next start, so a public endpoint never outlives its owner and a restart cannot leave two clients fighting over one reserved name. The user's intent is persisted in tunnelEnabled in ~/.warren/settings.json; after a restart the daemon restores the adapters that were left enabled, so Public Access recovers until the user explicitly disables it. Release apps may bundle gnar at Contents/Resources/gnar; Warren runs that worker with a separate ~/.warren/gnar credential directory and never migrates the system gnar store. WARREN_GNAR_PATH preserves explicit/system selection, while WARREN_GNAR_CONFIG_DIR overrides the child credential directory for operators and tests.
Daemon events (start/stop with build version, tunnel start, restore, and errors) are appended to ~/.warren/headless.log with 0600 permissions and rotate at 5 MiB; point --log-file or WARREN_LOG_FILE elsewhere or set it empty to disable file logging.
The Web UI and /v1/ws share port 8789; the local browser uses http://127.0.0.1:8789/#t=<token> and LAN devices use https://<host-LAN-IP>:8788/#t=<token> after trusting the local CA (see "LAN HTTPS"). Public Access is managed by the daemon: GET /v1/public-access reports the effective Edge URL, any saved custom override, effective/configured account label, credential-free authenticated/enabled/running state, and credential-free Public Endpoint; POST /v1/public-access/test saves the non-secret Edge/account configuration and verifies the gnar login without enabling the live endpoint. Settings should use this route for Save & Test. POST /v1/public-access/enable starts the live endpoint (and still accepts the older one-step Edge/account/key body for compatibility), while /disable and /restart control the lifecycle. POST /v1/public-access/reset stops Warren's local worker, clears the local Edge/account/enabled intent, and removes only the bundled Warren gnar credential directory; it never calls a remote release operation. Approval Key takes precedence when both keys are supplied. An omitted Edge URL keeps the current override; an explicit empty edgeUrl clears it and selects the release/launcher default. Approval Keys are sent to gnar login --edge <EDGE_URL> --account <ACCOUNT_NAME> --enrollment-key-stdin --json; Invite Keys use --key-stdin --json; both travel only through stdin and are never persisted. When account name is omitted, Warren derives a gnar v1.7-compatible value from --name/WARREN_HOST_NAME (or the system hostname). The lower-level GET /v1/tunnels and POST /v1/tunnels/start|stop routes remain for compatibility with existing clients; their legacy web_url token fragment is retained only for those clients and is not returned by Public Access. Legacy clients may still need that fragment to open a protected URL. Warren's explicit Public Access Open action adds the fragment only for that browser launch; the API, endpoint display, and copy surfaces strip it, so this compatibility path remains a residual credential exposure risk in browser history. The user’s custom Edge override and non-secret account label are configured through gnarEdge and gnarAccount in ~/.warren/settings.json (or WARREN_GNAR_EDGE) and can be read or updated with GET/PUT /v1/settings or the settings.get / settings.put WebSocket methods. Release builds inject the non-secret default Edge with WARREN_GNAR_DEFAULT_EDGE; source builds show https://tunnel.example.com until a release replaces it. That value is not persisted, so users without an override follow a new default after upgrading. Empty-workspace entry defaults are host settings: autoOpenShell and autoStartAI both default to false. Git worktree import is project-scoped: Project.autoImportGitWorktrees is opt-in, and project.worktrees plus project.worktrees.import expose the one-time selector path; project.autoImportGitWorktrees enables immediate, non-interactive import of all currently existing external worktrees for that project.
Operators can announce a planned restart with POST /v1/maintenance (Bearer
token required). The daemon broadcasts a {"t":"maintenance","state":"starting","message":...}
control message to every connected client, which then shows an "Updating"
state instead of treating the disconnect as a connection failure. The install
script calls this endpoint before replacing the daemon binary.
Output Pipeline
Ghostline owns the PTY output stream and exposes an atomic checkpoint (screen
replay plus cursor) for recovery. The Host broadcasts bounded DENB frames
(sessionID/epoch/sequence/payloadLength) and sends a synced marker only
after the checkpoint is complete. Every client has its own outbound queue; a
slow client only disconnects itself.
Runtime
warren-headless defaults to the
ghostline runtime: one
pseudo-terminal per session. Sessions are owned by a detached ghostline
server process (ghostline serve, spawned automatically on first start and
reconnected over ~/.warren/ghostline.sock), so daemon upgrades and restarts
never end sessions. A server-side libghostty-vt emulator renders screen
snapshots (visible grid + scrollback, SGR preserved) at the client's size.
The Host keeps a bounded in-memory output ring and a durable Ghostline cursor;
checkpoint recovery is atomic and clients still render with their own terminal
emulator. Input is written to the PTY
verbatim, and kitty-protocol keys
(for example Shift+Enter) reach the application unchanged.
Known limits:
- A forced ghostline server restart still ends its sessions (the server process owns the PTY masters). Protocol upgrades are rolled in place: the daemon starts a fresh server, adopts every session over the admin socket, and retires the old process without ending children. If adoption is not possible (for example a server predating the admin socket), the daemon keeps the old server running and retries on a later start.
- The release app bundles the v0.8 compatibility bridge and its arm64
libghostty-vt dylib for one-time v0 migration. A non-app installation that
still needs that bridge must provide it through
WARREN_GHOSTLINE_V0_COMPAT.
Agent Transcript Projection
For codex, claude, and opencode sessions, warren-headless projects the
provider's own activity into normalized agent events and sends them to
attached clients as {"t":"agent","session":...,"events":[...]} text messages.
Codex and Claude write JSONL transcripts (Codex:
~/.codex/sessions/**/rollout-*.jsonl, Claude Code:
~/.claude/projects/**/<session>.jsonl). OpenCode stores SQLite rows in
opencode.db.
Live batches are split so a single message stays around 256 KiB; the complete
Host-owned activity/attention status is its own lightweight
{"t":"agent.status","session":...,"status":{...}} message. The
contract is defined in
docs/rfc/0006-agent-activity-attention.md.
Attach only replays a bounded tail of the conversation, so clients
that need the full history fetch it page by page with the agent.history
request (session, optional before sequence cursor and limit, returning
events, cursor and hasMore). This keeps any single WebSocket message far
below client message-size limits even for transcripts with thousands of events.
The PTY byte stream remains the source of truth; the transcript is a
best-effort, read-only side channel.
OpenCode's data root follows WARREN_OPENCODE_DATA_DIR when an operator needs
to point Warren at a separate store, then the provider's platform data
directory (~/.local/share/opencode on macOS and Linux, or
%LOCALAPPDATA%\opencode on Windows). XDG_DATA_HOME/opencode is honored when configured. Warren opens
the SQLite database with a read-only connection and never creates or mutates
the provider database. If no usable SQLite schema is found, the Agent
projection waits for a later successful poll. Warren mirrors one bound
conversation into a private
JSONL cache under ~/.warren/opencode-cache/; mutable snapshots are compacted
periodically so a long response does not grow quadratically. The cache survives
a daemon restart for recovery and is removed when the Warren session is
explicitly deleted.
Each Warren session is bound to one CLI conversation by its own session ID, so several agents in the same workspace never mix transcripts:
- Claude starts with
--session-id <warren-session-id>, which makes its transcript path deterministic (~/.claude/projects/-Users-.../<id>.jsonl). - Codex and Claude get Warren-managed
SessionStartandSessionEndhooks merged into$CODEX_HOME/hooks.json/$CLAUDE_CONFIG_DIR/settings.json(user entries are preserved).SessionStartwrites the CLI'ssession_id/transcript_pathto~/.warren/agent-bind/<warren-session-id>.jsonand resets the state file;SessionEndmarks the state file exited so the status light returns to the surrounding shell. The daemon starts the watcher from the exact file. - Every Warren session, including a plain Shell tab, inherits the same
binding environment. A Codex/Claude CLI started manually inside that shell
is bound by the same hooks: the tab shows agent activity while it runs and
returns to a plain shell after
SessionEnd. Sessions created before this feature need to be reopened so the shell picks up the new environment.
OpenCode has no Warren hook binding. The daemon discovers a newly created OpenCode session by workspace and creation time, then persists both the Warren session ID and the OpenCode session ID. Subsequent polls use that exact ID, so two Warren sessions in one workspace cannot silently consume each other's conversation. If the provider database is unavailable or the schema changes, the terminal remains usable and the Agent projection simply waits for a future successful poll.
The bound CLI session ID and transcript path are stored on the Session and shown in the Web Agent view. For Codex and Claude, when a hook binding is not available yet (for example, an older CLI layout), discovery falls back to cwd and mtime matching. OpenCode remains unbound until its provider session can be identified safely.
The Web client renders an Agent view for these sessions and sends user input through the same PTY as terminal bytes. If a transcript is missing or its format changes, sessions keep working as plain terminals.
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
warren
command
|
|
|
warren-headless
command
|
|
|
internal
|
|
|
agent
Package agent watches the JSONL transcripts that Codex and Claude Code write while their TUI runs, and projects those files as normalized events.
|
Package agent watches the JSONL transcripts that Codex and Claude Code write while their TUI runs, and projects those files as normalized events. |
|
releaseconfig
Package releaseconfig contains values injected into Warren release binaries.
|
Package releaseconfig contains values injected into Warren release binaries. |
|
tlscert
Package tlscert manages a local CA and server certificate for the LAN HTTPS listener.
|
Package tlscert manages a local CA and server certificate for the LAN HTTPS listener. |