Desk runs every managed session under a Moor holder. The browser is a view over that durable process. Built-in agents add launch flags, resume behavior, permission handling, and attention signals on top of that Moor-managed lifetime.

Supported session kinds

Built-in agent values:
  • codex
  • claude
  • opencode
  • qwen
  • kimi
  • grok
  • pi
  • bash
Any session can also use a custom command.
Custom commands are terminal sessions. They do not get built-in agent resume or permission logic unless the command implements compatible behavior itself.

Session identity

Every session has a durable sessionId. Desk preserves it across edits and uses it for the Moor control socket, terminal state, channels delivery, attention, and agent events. The agent’s resume id remains a separate provider-owned value. Desk sets these environment variables for managed launches:
DESK_SESSION_GENERATION is what lets Desk reject a report from a producer that outlived a restart: a hook stamps the generation it was launched in, and the daemon refuses anything carrying an older one. A hook process that inherits no generation posts nothing — silence is the correct answer when an event cannot be attributed, and it is why a session started before its hooks existed reads unknown rather than reporting stale state. A session bound to an agent profile also receives that provider’s credential-directory variable — CLAUDE_CONFIG_DIR, CODEX_HOME, or PI_CODING_AGENT_DIR — pointing at the profile root.

Managed hook and adapter API

desk custody managed --request FILE|- sends a bounded JSON request to the existing backend’s POST /api/custody/v1/managed. It requires the caller’s own managed launch credential. An author name, hook session ID, permission mode or approved field cannot supply identity or authority. Operator lifecycle, acceptance and the existing GitHub administration routes remain operator-only. The version 1 envelope is {schemaVersion:1, requestId, kind, ...}. Use capabilities first and check its version and supported effects. An old, unavailable or incompatible runtime is a failed mandatory check. The wire schemas live in src/shared/managedRuntime.ts. Channels effects still execute through desk channels post/edit. The native store checks the effective sender, original edit owner and pinned current sources at commit. A previous effect.check cannot bypass a later Stop, source edit or changed launch binding. Managed GitHub intentions retain their authenticated execution; their existing executor repeats generation, Stop, source/grant and journal writer checks immediately before the effect. Version 1 refuses managed dispatch on a remote writer rather than assuming that its source execution remains live. Successful responses contain schemaVersion, requestId, requestDigest, kind and result. The digest binds the exact schema-parsed request; it is not an authorization token. requestId correlates responses. GitHub’s operationId remains the durable idempotency key. Before execute dispatch, the CLI durably retains the request and operation ID. If its profile state directory is unwritable, it uses a private temporary directory and reports that fallback; if neither is writable, it refuses dispatch. On a lost or refused response it reports the retained path. Read its outcome before retrying the same intention; do not invent a new operation ID. Lifecycle adapters may save this context alongside their private continuation state, full goal, corrections, approach history and pending request IDs. Those snapshots are restoration data, not a second assignment or permission ledger. Reading another managed session’s context/outcome is refused. Finishing a turn while waiting for a person or external owner does not claim goal acceptance; an unavailable finish observation must not start an infinite Stop-hook loop. This API does not intercept arbitrary shell, filesystem or network tools, and does not control effects performed with independent external credentials. Provider hook registration, actual invocation, child/resume/compaction delivery and failure behavior must be verified on each supported host/mode. Desk’s desk-agent-event hooks report lifecycle observations; their successful exit alone is not effect authorization.

Codex

Codex sessions launch as codex, with flags added for the features the session enables. Desk no longer configures Codex’s terminal notification settings: state comes from Codex’s own lifecycle hooks, so there is nothing to parse out of the TUI. When permission bypass is enabled, Desk adds Codex’s dangerous bypass flag. When resume is set, Desk launches Codex in resume mode. When LSP agent access is enabled, Desk passes a desk_lsp MCP server definition to Codex so the agent can call Desk’s language-server tools.

Claude

Claude sessions use Claude Code’s CLI and support:
  • resume ids
  • dangerous permission bypass
  • optional MCP config for Desk LSP tools
  • hook settings for state reporting and resume capture
Desk’s Claude hooks live in Desk’s own settings file and are handed to the CLI with --settings at launch:
Desk does not write ~/.claude/settings.json. That file is the operator’s — their model, their hooks, their preferences — and a tool that installs its reporting by editing it has taken something it was not given. The launch flag achieves the same thing for the session Desk starts and leaves the operator’s file exactly as they wrote it. desk hooks install writes Desk’s file.

OpenCode

OpenCode sessions launch through the configured OpenCode binary. Desk resolves it from:
  • DESK_OPENCODE_BIN
  • PATH
  • ~/.opencode/bin/opencode
Desk uses a Desk-owned OpenCode config directory, normally:
That directory contains Desk’s attention plugin. Desk does not rely on the user’s normal OpenCode config directory for its integration state. OpenCode permission behavior is controlled per session through OPENCODE_CONFIG_CONTENT:
  • bypass enabled: permission["*"] = "allow"
  • bypass disabled: permission["*"] = "ask"
Desk also sets:
This preserves Desk/xterm selection and copy behavior. OpenCode resume uses ses_... ids. Desk can discover recent OpenCode sessions from opencode session list --format json and persist a matching id.

Qwen

Qwen sessions launch as qwen (Qwen Code, a Gemini-CLI fork). Desk supports:
  • resume ids (--resume <id>, a UUID)
  • permission bypass (--yolo)
  • hook settings for state reporting and resume capture
Desk wires desk_lsp for Qwen in two parts. desk hooks install registers the server once in ~/.qwen/settings.json (Qwen has no per-session MCP flag), unconditionally — the install command has no view of LSP settings. The per-session part is gated: only when LSP agent access is enabled does a Desk launch carry the session’s DESK_LSP_ENV_FILE in the environment. Without that variable the registered tools stay visible to Qwen but every call returns a missing-environment error — including in Qwen sessions run outside Desk. Note that the variable lives in the Qwen process environment, so child processes Qwen spawns inherit it; the token it points to stays bound to the launching session’s workspace. Qwen’s hooks are Claude-compatible and live in:
desk hooks install merges Desk’s hooks into that file and also fills two terminal-context keys Qwen exposes only through settings — ui.mouseTracking: false and ui.useTerminalBuffer: false, so a click does not paste mouse-report bytes and the pane has one scrollback instead of two. It fills only keys the operator has not set: an explicit operator value is never overridden, and other settings are untouched. If the file is malformed JSON it is backed up and skipped rather than overwritten. Qwen reads a command hook’s timeout in milliseconds (unlike Claude/Codex, which use seconds), so Desk writes 10000 there. Permission bypass maps to Qwen’s --yolo (auto-approve all tools; needs Qwen Code ≥0.21.13 — older CLIs reject the flag, and Qwen’s own auto-updater keeps the effective version current). A successful --resume keeps the conversation id. What Qwen does not do is persist a resumable session before the first message is exchanged, and a fresh launch mints a new id — so a Qwen pane restarted before any input carries an id the CLI rejects. Desk keeps the pane alive (see Resume capture) rather than letting it exit. Qwen needs a provider credential. Point it at Alibaba ModelStudio, a third-party key, or any OpenAI-compatible endpoint through Qwen’s own Connect a Provider flow.

Kimi

Kimi sessions launch as kimi (Kimi Code). Desk supports:
  • resume ids (--session <id>; Kimi mints session_... values, which Desk validates as opaque tokens)
  • permission bypass (--yolo)
  • hook settings for state reporting and resume capture
Kimi’s hooks are TOML [[hooks]] blocks appended to:
desk hooks install rewrites only Desk’s own blocks and leaves operator-authored hooks in place; a config whose hooks key is not an array of tables is skipped rather than corrupted. Validate with kimi doctor. Kimi’s coding endpoint needs an active Kimi (Moonshot) membership for OAuth, or a provider key added through kimi provider.

Pi

Pi sessions launch as pi (pi.dev, the @earendil-works/pi-coding-agent CLI). Desk supports:
  • resume ids (--session-id <id>; Pi mints UUIDs, and the exact id is opened or created, so a pane restarted before its first message resumes cleanly)
  • state reporting and resume capture through a Desk-owned Pi extension
Pi has no hook files. Desk instead passes its own extension on the command line, and launches Pi in its regular (non-fullscreen) TUI mode so the pane’s own scrollback, mouse selection and copy keep working, the same reason Claude runs without its alternate screen:
desk hooks install writes that extension next to the event shim. It embeds the same producer runtime as the OpenCode plugin and posts to Desk in-process, in event order, without spawning anything. It listens to Pi’s session_start (with its reason, so a started or resumed pane reads idle at once), input (every submitted prompt, including one Pi queues while it is still streaming, which is what carries a Channels delivery id back), tool_call and tool_result, agent_end and agent_settled (a turn ends only when Pi will not continue on its own; a failed run reports its error class: rate limit, overloaded, authentication, billing, server error), and session_shutdown. Nothing is written into ~/.pi/agent. Pi has no per-tool approval system: every tool call runs without asking, so there is no permission-bypass flag and the bypass checkbox is hidden for it. Project trust (.pi/ extensions, settings, skills) is Pi’s own prompt and appears inside the pane. Pi uses its own provider logins (/login inside the pane; credentials live in ~/.pi/agent/auth.json). An agent profile points Pi at ~/.config/desk/profiles/<id> through PI_CODING_AGENT_DIR. That directory is Pi’s whole agent directory, not only its credentials: a fresh profile starts with no logins, no settings, and no packages, so install account-rotation extensions into it explicitly, for example PI_CODING_AGENT_DIR=~/.config/desk/profiles/<id> pi install npm:pi-multiprovider. Desk’s own extension is passed with -e and does not depend on that directory. A profiled launch unsets the provider keys Pi reads from the environment (the Anthropic, OpenAI, Z.AI, Gemini, Copilot, OpenRouter, xAI, Groq, Mistral, DeepSeek, Kimi, MiniMax, Qwen and other API-key variables from Pi’s provider table), so an ambient key cannot outrank the profile’s own login; each provider’s profile scrubs its own list. Channels deliveries are pasted into the Pi pane the same way as for Claude (bracketed paste, settle, Enter) and acknowledged through the extension’s input report. Pi’s project-trust prompt appears at startup in a folder with .pi/ resources; a delivery pasted while that prompt is open is not verified behaviour.

Grok

Grok sessions launch as grok (superagent-ai’s grok-cli, published to npm as grok-dev; it runs under Bun). Desk supports:
  • resume ids (--session <id>; Grok mints 12-hex values, which Desk validates as opaque tokens)
  • hook settings for state reporting and resume capture
Grok’s hooks are Claude-compatible and live in:
Grok has no per-tool approval system, so there is no permission-bypass flag and the bypass checkbox is hidden for it. Grok also fires SessionStart lazily on the first prompt (not at launch), so a freshly launched Grok pane reads unknown until the first message, and its tool hooks fire only for the bash tool. There is no periodic heartbeat to fill that gap: a long non-bash tool run (e.g. an MCP tool from Grok’s own settings) outlives the working lease and the session reads unknown until the next hook fires. Grok cannot join desk_lsp yet: it reads MCP servers only from its global settings and spawns them with a sanitized environment, so the per-session token cannot reach the server.
desk hooks install only writes a new agent’s hook config when that CLI’s config directory already exists, so a machine that never installed Qwen, Kimi, or Grok is left untouched.

Bash

Bash sessions run:
Bash does not have agent-specific permission bypass, resume capture, or LSP MCP wiring.

Permission bypass

The Add Session modal shows a bypass-permissions option for Codex, Claude, OpenCode, Qwen, and Kimi. The manifest field is:
For Codex and Claude, Desk maps that field to the agent CLI’s dangerous bypass mode; for Qwen and Kimi it maps to --yolo. For OpenCode, Desk maps it to a per-session OpenCode permission config. Unchecking the box makes OpenCode ask for tool permissions. Grok has no bypass flag (it has no per-tool approvals at all): the checkbox is hidden for it, and bypassPermissions: true on a Grok session is a manifest error rather than a silent no-op. (A bash session still ignores the field silently, as before.)

Resume capture

Desk can start a session without resume and later capture the conversation id:
  • Codex: reads Codex session records and startup shell snapshots
  • Claude, Qwen, Kimi, Grok: receive hook events carrying the provider session id
  • OpenCode: queries OpenCode’s session list
Captured ids are validated before writing the manifest. A resume id is a hint, not a guarantee. If the CLI rejects it — most often a Qwen session restarted before its first message, or any conversation the provider no longer has — Desk keeps the pane alive with a diagnostic and a shell instead of letting it exit. Start fresh by running the agent without a resume id, or clear the binding with desk reset-provider-session <sessionId> --force.

State reporting

Every supported CLI reports its own lifecycle to Desk through hooks that desk hooks install writes. Each hook posts a small, bounded observation — which event fired, and the few fields Desk needs from it — to /api/agent-event. Desk decides what that observation means server-side, so one definition of “working” serves every provider instead of one per adapter. The facts a provider can assert: Anything a provider does not assert stays unknown. Desk never fills the gap from terminal output: the OSC 9 and BEL sniffing earlier versions used is gone, along with the screen poller that backed it, because a signal that means “something happened” cannot be told apart from one that means “a turn ended”. Attention is a projection of that state for the operator. It does not gate channel delivery — see the delivery engine.
Hook configuration is read when a session launches. A session started before desk hooks install ran reports nothing until it is restarted, and shows unknown in the meantime.

Agent LSP access

When LSP is enabled and agent LSP access is enabled, Desk wires the desk_lsp MCP server into supported managed agents (Codex, Claude, and Qwen). The server exposes language-server tools such as hover, definitions, references, diagnostics, symbols, completions, rename preparation, rename edits, formatting, and code actions. The MCP surface is token-bound to the session workspace. Agents receive the token through a locked-down runtime env file or agent-specific MCP config. Tools return data and edits; they do not directly apply changes to files.

Channels membership

Channels give every managed agent session — Claude, Codex, OpenCode, Qwen, Kimi, and Grok — its own first-class member type. Custom-command and bash sessions enter through the generic bash-typed member path. The member type is a roster/notice label only — no delivery or supervisor behavior gates on it — so a bash-typed member still participates fully.