grithdocs

Supervisor sessions

List, inspect, create and terminate supervised sessions, plus the two session-scoped reads that sit outside this prefix.

Five routes, nested at /api/supervisor. This is the surface the dashboard's session list and its terminate button use.

MethodPath
GET/api/supervisor/sessions
POST/api/supervisor/sessions
GET/api/supervisor/sessions/{id}
POST/api/supervisor/sessions/{id}/kill
DELETE/api/supervisor/sessions/{id}

Two auth layers apply to all five: the dashboard token on every method once one exists, and x-grith-csrf on the three mutations. GETs use the general rate bucket, mutations the write bucket.

These are not the routes grith exec uses. A supervised CLI session registers itself over the daemon-token IPC surface at /api/ipc/sessions - see Daemon IPC.

GET /api/supervisor/sessions

{sessions, total}, where each entry is a summary:

One session summary
{
"id": "9c4f...",
"tool_name": "claude-code",
"project_name": "api",
"cwd": "/home/u/projects/api",
"tty": "pts/21",
"root_pid": 17421,
"uptime_seconds": 742,
"last_activity_seconds": 12,
"stats": {
  "total_intercepted": 88214,
  "total_allowed": 4702,
  "total_queued": 96,
  "total_denied": 14,
  "total_filtered_noise": 83402,
  "foreign_abi_denied": 0
},
"containment_remaining_seconds": 480
}

total_intercepted counts every trapped syscall including noise, so it dwarfs the other three. Allowed plus queued plus denied is the number of calls the proxy actually scored. last_activity_seconds is driven by scored calls only, so background noise does not keep a session looking busy. containment_remaining_seconds is absent unless the session is under a windowed containment.

GET /api/supervisor/sessions/{id}

id, tool_name, root_pid, uptime_seconds, process_tree_pids, the same stats, and containment_remaining_seconds when the session is contained. A non-UUID is 400 INVALID_ID; an unknown id is 404 SESSION_NOT_FOUND.

POST /api/supervisor/sessions

Starts a supervised session inside the daemon rather than from a terminal. tool_name is required; supply root_pid to attach to a running process, or command to spawn one. start_interception defaults to true.

POST /api/supervisor/sessions
{ "tool_name": "claude-code", "command": ["claude"], "start_interception": true }

The response is {id, tool_name, root_pid, interception_started}.

Admission is refused before anything is attached or spawned, which is the point: an empty tool_name, or start_interception with neither root_pid nor command, is 400 INVALID_REQUEST; a quarantined audit chain is 503 AUDIT_QUARANTINED and a read-only audit database is 503 AUDIT_READ_ONLY; no free seat under the licence ceiling is 429 SESSION_LIMIT_REACHED. A daemon that cannot record a decision does not start a session it would have to run unrecorded.

Ending a session

POST /api/supervisor/sessions/{id}/kill and DELETE /api/supervisor/sessions/{id} do the same thing - they share one handler: stop the interception task, remove the session from the registry, and return {id, status, tool_name, final_stats, interception_stopped} with status set to terminated.

Two session reads that are not under this prefix

MethodPathWhat
GET/api/inventory/{session_id}The binaries pinned at session start
GET/api/sessions/{session_id}/listener-rewritesEvery wildcard bind the supervisor clamped to loopback

Note the second path: /api/sessions/..., not /api/supervisor/sessions/.... Both are plain sensitive reads.

The inventory response carries session_id, binaries_pinned, total_scanned, truncated and entries of {path, sha256}. truncated means the walk hit its 5000-file cap and stopped short, which is a signal to narrow routine_exec_roots.

Each listener rewrite carries id, timestamp, pid, tool, original_addr, rewritten_addr and clamp_profile_entry - the profile entry that authorised the clamp. An empty list is the common case.

See also

Last updated: 2026-08-24Edit this page on GitHub →