Assistant Internal API
The Assistant Internal API provides capabilities for OpenCode agents to interact with the manager backend. Agents reach it through the ocm tool, which authenticates on their behalf; the HTTP surface itself is a secure bearer-token API.
For a user-facing overview of how to use and set up assistant mode, see Assistant Mode.
Authentication
The raw HTTP endpoints require a bearer token:
Authorization: Bearer <token>
The token has two in-product consumers, neither of which is an agent:
| Consumer | How it obtains the token |
|---|---|
Generated plugins (ocm-manager.js, ocm-sandbox.js, ocm-gh-env.js) | The OCM_INTERNAL_TOKEN environment variable, injected into the OpenCode child process |
External clients such as the ocm CLI | Settings → OpenCode Config → Server maintenance → Manager Internal Token, served by GET /api/settings/manager-token |
The generated plugins are OpenCode 2 plugin modules — plain, dependency-free objects that default-export { id, setup } and register their hooks and tools through the setup context. The Manager writes them to <config home>/opencode/plugins/; the legacy opencode/plugin/ files are removed on install.
Agents never authenticate against this API themselves. They call the ocm tool, which holds the token inside the Manager process.
The ocm Tool
The Manager installs a generated plugin (ocm-manager.js) that gives every agent an ocm tool. The tool calls this API from inside the Manager's own OpenCode process, so it needs no token, no base URL, and no network access from the agent shell.
That makes it the only path that works everywhere: a scheduled run executes in a throwaway worktree with no token of its own, and a sandboxed shell call runs in a microVM where localhost is the guest, not the Manager.
OpenCode 2 validates the tool's arguments against a JSON Schema before it calls execute, so the ocm tool declares its action and params shape as a JSON Schema derived from the same Zod definitions the tool uses.
The tool has two actions.
send_notification
Send a push notification to every device the user has registered.
{
action: 'send_notification',
params: { title: string, body: string, url?: string, tag?: string, priority?: 'normal' | 'high' }
}
request
Call an allow-listed internal API route and return the raw response body text.
{
action: 'request',
params: { method: 'GET' | 'POST' | 'PATCH' | 'DELETE', path: string, body?: object }
}
The path is relative to the internal API base (for example /settings or /repos/0/schedules) and query strings are allowed. The action resolves the path against the internal API base and rejects anything that resolves outside it (absolute URLs and .. traversal), then rejects any method plus path that is not in the allow-list.
Allow-listed routes:
GET /settings
PATCH /settings
GET /opencode-config
GET /opencode-config/effective
GET /opencode-config/mcp
PATCH /opencode-config
POST /assistant/reload
GET /repos
GET /repos/*/git-info
GET /opencode-workspaces
GET /schedules/all
GET /schedules/all/runs
GET /repos/*/schedules
POST /repos/*/schedules
GET /repos/*/schedules/*
PATCH /repos/*/schedules/*
DELETE /repos/*/schedules/*
POST /repos/*/schedules/*/run
GET /repos/*/schedules/*/runs
DELETE /repos/*/schedules/*/runs
GET /repos/*/schedules/*/runs/*
DELETE /repos/*/schedules/*/runs/*
POST /repos/*/schedules/*/runs/*/cancel
GET /sessions
POST /sessions
POST /sessions/*/prompt
GET /sessions/*/reply
POST /sessions/*/fork
Deliberately not allow-listed:
POST /notifications/send— use thesend_notificationaction instead.GET /git-credentials/gh-env— returns the GitHub CLI environment (GH_TOKEN,GITHUB_TOKEN) for a working directory for the OpenCode host process; it must not be readable by the agent.POST /sandbox/shell— the sandbox planner's internal route that resolves and pins the shell for ashellcall; exposing it would let the agent drive shell planning directly./repos/*/mirror/*— the repo mirror protocol that theocmCLI uses to sync entire repositories; it can create, replace, patch, or delete whole repos, so it stays reserved for the CLI.
Agents should use the ocm tool rather than the raw bearer-token endpoints below. Those endpoints remain available to the frontend, generated plugins, and other Manager-internal clients.
Endpoints
Notifications
POST /api/internal/notifications/send
Send push notifications to the user's registered devices. Prefer the ocm tool with the send_notification action.
Request Body:
{
title: string // 1-120 characters
body: string // 1-500 characters
url?: string // Optional: deep link (1-500 chars)
tag?: string // Optional: notification tag (max 80 chars)
priority?: 'normal' | 'high'
}
Query Parameters:
userId(optional): Defaults to"default"
Response:
{
delivered: number
expired: number
failed: number
noSubscriptions: boolean
}
Rate Limiting: 10 requests per minute per token. Returns 429 Too Many Requests with Retry-After header when exceeded.
Status Codes:
200: Notification sent400: Invalid request body401: Missing or invalid bearer token429: Rate limit exceeded503: Push notifications not configured (missing VAPID)
Settings
GET /api/internal/settings
Retrieve the user's full settings and preferences.
Query Parameters:
userId(optional): Defaults to"default"
Response:
{
preferences: {
theme: 'dark' | 'light' | 'system',
colorTheme?: string, // 'manager' or an OpenCode theme id from shared/src/themes; unknown stored ids read as 'manager'
mode: 'plan' | 'build',
defaultModel?: string,
defaultAgent?: string,
autoScroll: boolean,
expandDiffs: boolean,
expandToolCalls: boolean,
showReasoning: boolean,
simpleChatMode: boolean,
leaderKey?: string,
directShortcuts?: string[],
keyboardShortcuts: Record<string, string>,
customCommands: Array<{ name: string; description: string; promptTemplate: string }>,
notifications?: { enabled: boolean; ... },
repoOrder?: number[],
repoSortMode: 'recent' | 'manual' | 'name',
gitCredentials?: [...], // Read-only
gitIdentity?: {...}, // Read-only
gitIdentities?: [...], // Read-only
tts?: {...}, // Read-only
stt?: {...}, // Read-only
},
updatedAt: number
}
PATCH /api/internal/settings
Update a subset of safe user preferences.
Allowed Keys: The following preference keys can be modified:
theme,colorTheme,mode,defaultModel,defaultAgentautoScroll,expandDiffs,expandToolCalls,showReasoningsimpleChatMode,leaderKey,directShortcutskeyboardShortcuts,customCommands,notificationsrepoOrder,repoSortModecolorThememust bemanageror a catalog theme id; unknown ids return 400.tts— Non-secret TTS preferences (enabled,provider,autoPlay,voice,model,speed). TTS must already be configured in the UI (the endpoint returns 400 otherwise).stt— Non-secret STT preferences (enabled,provider,model,language). STT must already be configured in the UI (the endpoint returns 400 otherwise).
Restricted Keys: The following keys are NOT allowed and will be rejected:
gitCredentials- Git credentials must be managed via the full UIgitIdentity- Git identity must be managed via the full UIgitIdentities- Git identity presets must be managed via the full UItts.apiKey- TTS credentials must be managed via the full UItts.endpoint- TTS endpoint must be managed via the full UIstt.apiKey- STT credentials must be managed via the full UIstt.endpoint- STT endpoint must be managed via the full UIlastKnownGoodConfig- Internal state, do not modify
Request Body: Partial object with any of the allowed keys.
Response: Returns the updated settings object.
Status Codes:
200: Settings updated400: Invalid request body or disallowed key401: Missing or invalid bearer token
OpenCode Configuration
The global OpenCode configuration files in the workspace .config/opencode/ directory are the source of truth, and these endpoints are the only supported way to change them. The two sources OpenCode 2 reads are merged in order — opencode.json, opencode.jsonc — with later files overriding earlier ones. A legacy config.json is folded into a recognized source and archived; it is never read as a live source. The schema accepts both V1-compatible keys and native OpenCode 2 fields, so a V2-native config passes validation. The endpoints apply the same rules as the Settings UI: any semantic change is written to disk and applied to the running server, and it is flagged as restart required only when that apply fails; comment-only edits do nothing, and mcp changes go through the same location reload, which reconnects only the servers whose configuration changed.
The agent-facing surface is redacted: every read and write response replaces secret values with <redacted> and omits the raw source text, a write containing <redacted> is rejected, and PATCH changes only the paths it names, so an agent never has to read or reproduce the whole file. The public surface the Settings UI uses keeps full fidelity and the whole-document PUT.
GET /api/internal/opencode-config
Read the merged persisted configuration and its source files, with secret values redacted and the raw source text omitted. This is not the running instance configuration: project overrides and expanded environment values are not included.
Response:
{
path: string // Absolute path of the preferred write target
content: object // Merged configuration across all sources, secrets replaced with "<redacted>"
sources: Array<{ // Recognized source files, in merge order, raw content omitted
name: 'opencode.json' | 'opencode.jsonc'
path: string
content: object
isValid: boolean
validationIssues?: Array<{ path: string, message: string }>
updatedAt: number
}>
revision: string // Hash of the source set; send back as expectedRevision
isValid: boolean // Whether every source passes schema validation
validationIssues?: Array<{ path: string, message: string }>
updatedAt: number // Newest source mtime
redactedPaths: string[] // Dotted paths whose values were replaced
}
Status Codes:
200: Configuration state returned401: Missing or invalid bearer token404: No config source found500: Server error
GET /api/internal/opencode-config/mcp
List the configured MCP servers with their stored shape, enabled state, and live connection status. Header and environment values are never returned.
Response:
{
revision: string | null
servers: Array<{
name: string
type: 'local' | 'remote'
command?: string[] // For local servers
url?: string // For remote servers
enabled: boolean
shape: 'servers' | 'legacy' // Native mcp.servers.<name> or flat mcp.<name>
status?: 'connected' | 'pending' | 'disabled' | 'failed' | 'needs_auth'
error?: string
}>
}
Status Codes:
200: MCP server list returned401: Missing or invalid bearer token500: Server error
GET /api/internal/opencode-config/effective
Read the configuration OpenCode resolves for the workspace location as entries: the configuration documents and discovery directories in precedence order, lowest first, each shaped as { type: 'document', path, info } or { type: 'directory', path }. Its info values are expanded for the running server, so never copy this response into a save. Secret values in info are replaced with <redacted>.
Status Codes:
200: Effective configuration returned401: Missing or invalid bearer token502: OpenCode returned an error503: OpenCode server unavailable
PATCH /api/internal/opencode-config
Change only the paths the request names. Only those paths are patched into the preferred existing source (opencode.jsonc > opencode.json; a new installation gets opencode.jsonc); comments, unknown keys, and untouched inherited values are preserved. A null value removes a path. Removing a path defined only in a lower-priority source is rejected (409).
The public route keeps the whole-document PUT for the Settings UI; the internal route exposes PATCH instead.
Request Body:
{
patch: object // Nested paths to set; null removes a path
expectedRevision?: string // From GET; a stale value is rejected with 409
source?: 'opencode.json' | 'opencode.jsonc'
}
Response:
Returns the refreshed redacted configuration. Semantic changes, including mcp, are applied with an OpenCode location reload, without a restart; only the MCP servers whose configuration changed reconnect. Adds restartRequired: true only when that apply fails.
Status Codes:
200: Configuration written400: Invalid request body, invalid configuration, a source file that is not valid JSON/JSONC (sourceslists them), or a value equal to<redacted>(pathslists them)401: Missing or invalid bearer token409: StaleexpectedRevision(expectedRevision/actualRevisionin the body), or the patch would remove a value defined only in a lower-priority source (paths/sourcesin the body)500: Server error
Assistant
POST /api/internal/assistant/reload
Reload the OpenCode server configuration, rebuilding every loaded location. Use this after editing .opencode/agents/assistant.md or opencode.json so changes take effect on the next message. Returns 400 with validationIssues when the OpenCode configuration is invalid.
Rate Limiting: 5 requests per minute per token. Returns 429 Too Many Requests with Retry-After header when exceeded.
Example:
{
"action": "request",
"params": {
"method": "POST",
"path": "/assistant/reload"
}
}
Response:
{ "success": true }
Status Codes:
200: Assistant workspace reloaded400: OpenCode configuration is invalid (validationIssuesin the body)401: Missing or invalid bearer token429: Rate limit exceeded502: Failed to reload (upstream OpenCode error)
Repos
GET /api/internal/repos
Retrieve a list of all managed repositories, ordered by the user's repo preference order.
Response:
{
repos: Array<{
id: number
repoUrl?: string // Git remote URL (absent for local-only repos)
localPath: string // Relative path under repos root
fullPath: string // Absolute filesystem path
sourcePath?: string // Source worktree path (for worktrees)
branch?: string // Current branch name (for worktrees)
defaultBranch: string // e.g. "main"
cloneStatus: 'cloning' | 'ready' | 'error'
clonedAt: number // Timestamp when repo was cloned
lastPulled?: number // Timestamp of last pull
lastAccessedAt?: number // Timestamp of last access
isWorktree?: boolean // Whether repo is a worktree
isLocal?: boolean // Whether repo is local-only
}>
}
Status Codes:
200: Repository list returned401: Missing or invalid bearer token500: Server error (database failure)
Sessions
These routes back the session-management skill, so an agent can start work in a repo, hand a task to a new session, follow it up, read its reply, and fork it. Session creation always goes through the Manager's session launcher, which validates the repository and the requested model before creating anything. Malformed JSON is rejected with { "error": "Invalid JSON" }; a body that fails validation is rejected with { "error": "<first validation message>", "details": [...] }; unknown upstream failures return 502.
GET /api/internal/sessions
List sessions, newest first. Pass repoId to restrict the list to one repo — this covers every OpenCode workspace of that repo, not only the repo directory — and limit (1-50, default 10) to bound it.
Query Parameters:
repoId(optional): Restrict to a repository. An unknown id returns404.limit(optional): 1-50, default 10.
Response:
{
sessions: Array<{
id: string
title: string | null
directory: string
repoId: number | null // null only when the directory belongs to no known repo
busy: boolean // true while the session is running
outcome: 'succeeded' | 'failed' | 'interrupted' | null
updated: number
}>
}
Status Codes:
200: Session list returned400: Invalid query401: Missing or invalid bearer token404: Repository not found502: OpenCode error
POST /api/internal/sessions
Create a session in a repo and send the first prompt. The response returns as soon as the prompt is queued, not when the run finishes; poll GET /api/internal/sessions/:sessionId/reply for the result. Pass worktree: true to run in a new isolated workspace, with ref selecting its base ref. A requested model must be available; an unavailable model is rejected with 400 rather than silently substituted. Sessions created here are pinned to ask permission mode. With worktree: true, creation can take tens of seconds; a request that times out may still have succeeded, so check GET /api/internal/sessions before retrying.
Request Body:
{
repoId: number
prompt: string // 1-20000 characters
title?: string // max 200 characters
model?: string
agent?: string
worktree?: boolean
ref?: string
}
Response (201):
{
sessionId: string
repoId: number
directory: string
workspaceDirectory: string | null
model: string
title: string | null
url: string // deep link to open the session in the UI
}
Status Codes:
201: Session created and prompt queued400: Invalid request body, or the requested model is unavailable401: Missing or invalid bearer token404: Repository not found502: OpenCode error
POST /api/internal/sessions/:sessionId/prompt
Queue a follow-up prompt for an existing session.
Request Body:
{
text: string // 1-20000 characters
}
Response (202):
{ queued: true }
Status Codes:
202: Prompt queued400: Invalid request body401: Missing or invalid bearer token404: Session not found502: OpenCode error
GET /api/internal/sessions/:sessionId/reply
Read the latest assistant reply for a session and whether it is still running. Poll this after creating a session or queueing a prompt until busy is false. Pass waitMs (0-45000) to wait until the session settles instead of polling in a loop; the response returns when it settles or the timeout elapses. responseText is capped at 20,000 characters, with a truncation marker appended when it is longer.
Response:
{
busy: boolean
responseText: string | null // latest assistant text, null when there is none yet
errorText: string | null // assistant error message, null when the turn succeeded
completed: boolean // true once the latest assistant turn finished
}
Status Codes:
200: Reply state returned400: InvalidwaitMs401: Missing or invalid bearer token404: Session not found502: OpenCode error
POST /api/internal/sessions/:sessionId/fork
Fork a session, optionally before a specific message. Omit beforeMessageId to fork from the current point. The forked session is pinned to ask permission mode.
Request Body:
{
beforeMessageId?: string
}
Response:
{
sessionId: string // the new forked session
directory: string
}
Status Codes:
200: Session forked400: Invalid request body401: Missing or invalid bearer token404: Session not found502: OpenCode error
There is no delete route for sessions. Deleting sessions and workspaces stays out of this tool, as does changing permission modes and starting goals.
Skills
The assistant workspace includes five skills that document these capabilities:
- Schedule Management (
.opencode/skills/schedule-management/SKILL.md) — manage schedule jobs and runs through theocmrequestaction. - Notifications (
.opencode/skills/notifications/SKILL.md) — send push notifications through theocmsend_notificationaction. - Manager Settings (
.opencode/skills/manager-settings/SKILL.md) — read and patch user preferences, read and update the OpenCode configuration file, and reload the assistant workspace through theocmrequestaction. - Repo Management (
.opencode/skills/repo-management/SKILL.md) — list managed repositories through theocmrequestaction. - Session Management (
.opencode/skills/session-management/SKILL.md) — list, create, follow up, read the reply of, and fork sessions through theocmrequestaction.
These skills are automatically provisioned when assistant mode is initialized and contain detailed examples and usage patterns.