Documentation
¶
Overview ¶
hook-forward subcommand implementation. Agent CLIs (Claude/Codex/Grok) invoke this from their settings.json hooks config; it reads the hook payload from stdin, attaches the per-install secret, and POSTs to the daemon's localhost ingest endpoint.
Replaces Mac's Node.js hook script (~/.rmote/hooks/rmote-session-sync.cjs) with a single static binary subcommand — no Node dependency on the host, which matters for minimal Linux images and Alpine.
Hook configuration is installed by the settings patcher; this package ships the subcommand itself so manual installs work end-to-end.
Package hooks is the daemon's in-memory event bus for agent hook events.
Hook events arrive via POST /api/hooks/{agent} (called by the `rmoted hook-forward` subcommand which agent CLIs invoke from their settings.json hooks config). The bus broadcasts to all subscribers — iOS AgentDaemonClient subscribes via GET /api/agent/events long-poll and filters by its scraped agent_session_id.
This is dumb fan-out: the daemon does not infer which client tab owns a session. Clients perform per-tab attribution from the agent session ID.
Drop-oldest semantics on overflow: metadata events are low-throughput (a few per agent turn), but a misbehaving hook script could flood. Cap per-subscriber buffer at 16; lost events surface as a "lagged" warning in a client (currently log only; clients can resync via /api/sessions).
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func HookForward ¶
HookForward is the entry point for the `rmoted hook-forward` CLI subcommand. agentName is the CLI arg (--agent claude/codex/grok); stdin is the hook payload (Claude passes JSON, Codex/Grok may pass JSON too).
Returns nil (exit 0) when there is nothing to forward OR the daemon is absent — hook-forward is fire-and-forget and a stopped/never-installed daemon is a normal user state, not an error worth polluting the agent CLI's stderr on every invocation. Genuine failures (bad request build, daemon returned 4xx/5xx, transport timeout mid-handshake) still return non-nil so the agent CLI logs them.
func ParseAgentFlag ¶
ParseAgentFlag extracts --agent VALUE from a CLI arg slice. Returns "" if absent. Used by main.go to dispatch `rmoted hook-forward --agent X`.
func ParseEventFlag ¶
ParseEventFlag extracts --event VALUE from a CLI arg slice. Returns "" if absent. Used by Antigravity's per-event hook commands to tell hook-forward which event fired (its payload carries no event-name field).
Types ¶
type Bus ¶
type Bus struct {
// contains filtered or unexported fields
}
Bus is a fan-out broadcast bus. Goroutine-safe via a single mutex; the critical sections are tiny (map ops) so contention is negligible at the daemon's expected scale (1-10 subscribers per host).
func (*Bus) Publish ¶
Publish fans out to every subscriber. Slow subscribers (buffer full) silently drop the oldest event via the select-default below. This is intentional — a stuck iOS tab must not block the daemon's hook ingest.
func (*Bus) Subscribe ¶
Subscribe returns a buffered channel. Caller MUST call Unsubscribe when done to avoid leaking the channel (and the buffer). Long-poll handler pairs these in a defer.
func (*Bus) Unsubscribe ¶
type Event ¶
type Event struct {
Event string `json:"event"`
SessionID string `json:"session_id,omitempty"`
RmoteSessionID string `json:"rmote_session_id,omitempty"`
Title string `json:"title,omitempty"`
Body string `json:"body,omitempty"`
ToolName *string `json:"tool_name,omitempty"`
EventAt *float64 `json:"event_at,omitempty"`
Project *string `json:"project,omitempty"`
Agent string `json:"agent"`
Timestamp int64 `json:"timestamp"`
}
Event is the wire shape for both POST /api/hooks/{agent} request body and GET /api/agent/events response. Field names mirror iOS's NotifyPayload so iOS can reuse its decoder regardless of transport.
The daemon sets Agent (from the URL path) and Timestamp (server time, milliseconds since epoch, for "since" long-poll filtering). All other fields come from the agent CLI's hook payload.
Two session-identifier fields exist because they identify DIFFERENT things:
- SessionID: the agent CLI's own session UUID (Claude's from ~/.claude/projects, Codex's from ~/.codex/sessions). iOS scrapes this from terminal output to match hook events to tabs.
- RmoteSessionID: the daemon's tab UUID (injected via RMOTE_SESSION_ID env at PTY spawn). Lets iOS match events to tabs WITHOUT scraping — load-bearing for backgrounded tabs (C2 regression) where the scrape suspends. Populated by hook-forward reading $RMOTE_SESSION_ID.