Overview

graith is a daemon/client system. A long-lived daemon (graithd) owns PTY sessions and persists state. A stateless CLI client (gr) connects over a Unix socket.

graph LR
    cli["gr (CLI)<br/>client"] <-->|"frames · control + data"| daemon["graithd<br/>daemon"]
    daemon <-->|PTY| agents["agents"]
    daemon --> persist["state → state.json<br/>msgs → messages.sqlite<br/>logs → scrollback files<br/>store → flat-file git repos"]

Wire protocol

The protocol uses 5-byte framed multiplexing:

  [channel:1 byte][length:4 bytes big-endian][payload:N bytes]
  

Three channels:

ChannelPurpose
0x00JSON control messages
0x01Raw PTY data
0x02MCP proxy traffic

Control messages use a JSON envelope:

  {"type": "create", "payload": {"name": "fix-bug", "agent": "claude", ...}}
  

The client sends a message type; the daemon responds with a corresponding response type or error. The protocol is versioned (currently 1.0) and checked during handshake.

Message types

Client to daemon:

TypePurpose
handshakeInitial connection setup (version, terminal size, cwd)
createCreate a new session
forkFork a session
attachAttach to a session
detachDetach from a session
deleteDelete a session
renameRename a session
star / unstarStar/unstar a session
stopStop a session
resumeResume a stopped session
restartRestart a session
listList all sessions
logsStream session output
typeType into a session’s PTY
resizeResize the PTY
upgradeUpgrade daemon binary
msg_pubPublish a message
msg_subSubscribe to messages
msg_ackAcknowledge messages
msg_topicsList message topics
set_statusSet session status summary
status_reportReport agent hook status
approval_listList pending approvals
approval_respondRespond to an approval request
mcp_connectConnect to an MCP server
reloadReload config
diagnosticsRequest diagnostics
screen_previewRequest screen preview
screen_snapshotRequest screen snapshot

Daemon to client:

TypePurpose
handshake_okHandshake successful
session_listList of sessions
errorError message
detachedSession detached
logs_doneLog streaming complete
msg_messageMessage data
msg_doneMessage streaming complete
msg_followingFollowing mode active
approval_notificationApproval notification
approval_decisionApproval decision
status_setStatus set confirmation
screen_preview_responseScreen preview data
screen_snapshot_responseFull screen snapshot
mcp_connect_okMCP connection established
status_responseStatus query response
diagnosticsDiagnostics data

Daemon

The daemon (SessionManager) is the central component:

  • Session lifecycle: create, stop, resume, restart, delete, fork
  • PTY management: spawning, resizing, I/O multiplexing
  • State persistence: state.json loaded on start, saved on mutations
  • Worktree management: creating, removing git worktrees and branches
  • Client handling: connection acceptance, frame demuxing, message dispatch
  • Hook reporting: tracking agent status from hook reports
  • Approval handling: managing approval requests and responses
  • MCP management: proxying MCP connections for sessions
  • Git pull: periodic background pulls (when enabled)
  • Idle timeout: automatic session stopping after inactivity

State persistence

state.json stores session metadata: ID, name, agent type, repo path, worktree path, branch, status, parent ID, starred flag, and timestamps. It is loaded on daemon start and written synchronously on every mutation.

Runtime-only state (hook reports, attached clients, pending approvals, in-memory caches) is not persisted and is rebuilt from PTY state on restart.

State-version backups. The state file carries a schema version, and a newer daemon migrates an older state.json forward in place. Because a downgraded (older) binary refuses to start against forward-migrated state, the daemon writes a crash-safe copy of the pre-migration file to state.json.v<oldVersion>.bak (alongside state.json) before migrating. Only the most recent pre-migration backup is kept — an earlier one is removed once the new backup is durable. To recover a downgrade, stop the daemon, restore the backup over state.json (e.g. mv state.json.v16.bak state.json), and start the older binary. gr doctor lists any available backups.

Session manager locking

The SessionManager uses a sync.RWMutex for concurrent access. Read operations (list, info) take a read lock. Mutations (create, delete, stop) take a write lock. The handler dispatches all control messages through the session manager.

Client

The client is stateless. On each command, it:

  1. Connects to the Unix socket
  2. Sends a handshake (version, terminal size, working directory)
  3. Sends the command-specific control message
  4. Reads the response
  5. Disconnects

For attach, the client enters a loop:

  1. Passthrough mode: raw terminal I/O forwarded to/from the daemon
  2. Overlay mode: session picker TUI (Bubble Tea)
  3. Shell mode: interactive shell in the worktree

The client transitions between these modes based on prefix key commands and overlay actions.

Passthrough

In passthrough mode:

  • Terminal is set to raw mode
  • stdin is read in a goroutine and forwarded to the daemon on channel 0x01
  • Daemon output on channel 0x01 is written to stdout
  • Control messages on channel 0x00 are processed (detach, approval notifications, status updates, screen snapshots)
  • The prefix key intercepts the next keystroke for commands
  • SIGWINCH signals trigger PTY resize messages

Overlay

The overlay is a full-screen TUI built with Bubble Tea. It renders session lists with filtering, view modes, and a live preview panel. The preview uses vt10x to parse terminal output into a rendered screen.

PTY management

Each session has a PTY session (pty/session.go) that:

  • Spawns the agent process with a pseudoterminal
  • Multiplexes PTY output to: the attached client (if any) and the scrollback file
  • Handles resize signals
  • Manages process lifecycle (start, stop, restart)

Scrollback

PTY output is appended to a scrollback file (pty/scrollback.go). The file supports:

  • Append-only writes (concurrent-safe)
  • Tail reads (for gr logs --lines N)
  • Follow reads (for gr logs --follow)

Messaging

The messaging system (daemon/msgstore.go) uses SQLite:

  • Messages are stored with stream name, body, sender info, thread ID, reply-to, and timestamp
  • Subscriber positions are tracked per-stream for unread counting
  • --wait and --follow use polling with notification channels
  • Retention is enforced by max_age and max_per_stream

Document store

The store (store/store.go) operates directly on disk:

  • Each store is a git repository (initialized with git init)
  • Writes are committed with git add + git commit
  • Deletes are committed with git rm + git commit
  • Keys are validated against a strict set of rules (no path traversal, no special characters)
  • Per-repo stores are named <repo-name>-<hash> using the canonical repo path
  • The shared store uses a fixed shared directory

Sandbox

When sandbox is enabled (sandbox/sandbox.go), the backend is pluggable — selected with [sandbox] backend (safehouse = macOS sandbox-exec; nono = Landlock+seccomp on Linux / Seatbelt on macOS). backend is required when the sandbox is enabled; an unset backend fails closed.

  1. The daemon resolves the merged sandbox config (global + per-agent)
  2. ~ paths and globs are expanded to absolute
  3. The command is wrapped by the selected backend — either safehouse wrap (sandbox/safehouse.go) or a generated per-session nono JSON profile run via nono run --profile (sandbox/nono.go)
  4. The daemon checks the selected backend can enforce before session creation (binary present, kernel/version adequate, network policy supported); if not, creation fails closed

Agent detection

The agent/agent.go module detects AI agent environments by checking for environment variables: GRAITH_SESSION_ID, CLAUDECODE, CLAUDE_CODE, CURSOR_AGENT, GITHUB_COPILOT, AMAZON_Q, OPENCODE. When detected, JSON output is auto-enabled. Override with GR_AGENT_MODE=0 or GR_AGENT_MODE=1.

Package layout

  cmd/graith/              Entry point (main.go)
internal/
  agent/                 Agent environment detection
  cli/                   Cobra command definitions (one file per command)
  client/                Client: connection, passthrough, overlay, shell, status bar
  config/                TOML config loading, defaults, XDG paths
  daemon/                Daemon: session manager, handler, state, server, messaging, MCP manager
  detector/              Agent type detection from running processes
  git/                   Git operations (fetch, worktree, branch)
  hookoutput/            Agent-specific hook response formatting
  integration/           Integration tests (spawn real daemon)
  mcp/                   MCP server implementation
  output/                Structured output helpers (text/JSON)
  protocol/              Wire protocol: framing, control messages, encoding
  pty/                   PTY session management, scrollback buffer
  sandbox/               Pluggable OS sandbox backends (safehouse, nono)
  store/                 Flat-file git-backed document store
  version/               Build-time version injection
  

All packages are under internal/. There is no public Go API.