Local API
Edit and impact requests take JSON.
- Every edit and preview names its
audience:sharedorpersonal. - The older
scopefield (teamorlocal) is accepted until the next minor release, except on/api/edit/mcp,/api/edit/hook,/api/edit/permissions, and/api/edit/protected. A request that sends both with different meanings returns 400. - A change plan carries both
audienceandscopeuntil then. - A boolean edit must include its
enabledorvaluefield, even when false. - Invalid values, and YAML that cannot be spliced safely, are refused without changing the file.
- Previews hide recognized credentials using the credential preview rules. They run no MCP servers, hooks, or external adapters.
GET /api/health
Takes no input.
- Returns
ok, absoluteroot,global,ui(whether the web interface is served), andversion(asagnostic-ai --versionprints it). It does not load specs. - A global server started in a project adds that project's absolute
projectRoot. - When the config sets
requires, it addsrequiresas written andrequiresMet:trueorfalse, ornullfor a build that is not a release. It reads both on each request.
POST /api/switch
Body: {} with Content-Type: application/json.
- Finds or starts the server for the other configuration: the user configuration from a project, or
projectRootfrom a global server. - Returns
url,root,global, andstarted. - Answers 404 on a global server with no
projectRoot. - Answers 409 with
{error, missing, root}and no server when the user configuration folder has no setup. - Answers 503 with the command to run when no server could start.
POST /api/global/create
Body: {"git": true} or {"git": false}. Project servers only.
- Writes the starter files into the user configuration folder fixed at startup.
- Replies
{root, created, git}. - Answers 409 when a setup already exists.
- Any other field is a 400.
GET /api/views/overview
Takes no input.
globalis true for user configuration.repositoryis{url, name}when the project'soriginremote is on GitHub.urlis the repository'shttps://github.com/<owner>/<repo>page andnameis<owner>/<repo>. Absent otherwise, and for user configuration.health.gapslists what the configured tools support and the project does not use yet. Each is{id, section, label, supported, missing, fix}. Built-in specs fill no gap. Empty for user configuration.idiskind:<kind>(rule, agent, skill, command, review or ignore with no spec of the project's own),stage:<stage>(a hook stage with no hook) orbuiltin:handoff(off).supportedlists the configured targets that would use it.fixis a prompt a coding agent can run.
health.improvementsis{applied, total}.totalcounts every kind, stage, and built-in the configured targets support. Each one not applied is a gap, sototal - appliedequals the number of gaps. Absent for user configuration.
GET /api/views/context
Query: ?target=<tool>, a configured target. The default is the first one.
&file=<path>adds scoped rules for that file, as--filedoes.fileTargetslists the targets that accept it.- Returns
targets,target, absoluteroot,startupWordBudget(thelint.instructions-wordsvalue the page compares startup words with), andreport, theexplain --context --jsonoutput. - With
--global,supportedis false andreasonsays why.
GET /api/views/source
Query: ?path=<absolute path>, the openPath of a row the Context or Lifecycle page lists. That row is an instructions file, rule, agent, skill, command, review instructions, ignore file or hook of the served configuration, project or global, including local/.
Returns:
pathandkind.frontmatter: a list of{key, value}in file order.body.bytes: the file size.truncated: true when the body was cut at 256 KB.withheld: true when the text is hidden. Thenbodyandfrontmatterare empty. The text is hidden in two cases:- It may hold a credential: any credential-named key, or any of the words password, passphrase, secret, credential, API key, private key, access key, secret key, or client secret, even in guidance that only names them.
- The file is too large to check whole (over 320 KB).
Rules:
- A leading frontmatter block is read whole, up to 64 KB more, so its values can be checked. One that does not close within that is withheld.
- A hook returns every field of its YAML file in
frontmatterand an emptybody, withheld by the same rule. A hook file that does not parse returns no fields. - Any other path returns 404. So does a file whose symlinks lead outside the source folders the configuration loads.
GET /api/views/conversations
Query:
?editor=<id>: the editor chosen in Settings, so editor links use its scheme.&before=<time>: the next page, which holds older conversations.&q=<words>: a search.&tool=claude,codexorcursor: list one tool.
A search keeps the conversations of every age where each word appears in the title, folder, branch, latest prompt, tool or app, as the row shows them. Case is ignored.
- A withheld field does not match.
- Its pages hold only matches, and the page adds
matches, the count across all pages. - A
qover 200 characters, or anothertool, is a 400.
The route lists Claude Code, Codex and Cursor conversations from each tool's own store. The newest come first, by the last message from the person or the agent.
- Each page holds one week. The first page holds every open conversation and the past week.
- A page asked for with
beforeholds the week before that time, and reaches past weeks with no conversation. - The page returns
fromandto, andnext, thebeforevalue of the following page, absent when no older conversation remains. - A
beforethat is not an RFC 3339 time is a 400. - A project server lists the conversations started in the project or one of its Git worktrees. A global server lists them all.
- Runs a program started (Claude Code SDK sessions,
codex exec) are left out.
Returns tools and conversations:
toolsis{tool, label, found, count, shown, note?}.countis every conversation the tool holds,shownis how many this page holds, andnotesays why a store was not read.- Each of
conversationsis{tool, app, id, title, folder?, branch?, startedAt?, updatedAt, opening?, prompt?, withheld?, live?, activity?, open}.
Conversation fields:
appiscli,ide,desktop,backgroundorcloud.folderis where the conversation started and where it resumes.liveis true while it is open: a running Claude Code process holds it, a Codex process has its thread loaded, a Cursor window shows it selected, or a cloud provider reports an active session.activityisworking,waitingoridlewhen available. Claude Code reports local session status. Local Codex activity is inferred from its rollout. Cloud sessions use provider status.openingholds up to three lines of the first prompt, andpromptholds the latest one.- Any of
title,openingandpromptthat may hold a credential is left out and named inwithheld, by the rule/api/views/sourceuses. openis{kind, label, command?, link?}:focusswitches to the Terminal tab that runs it.terminalrunscommandin a new Terminal window.linkopens the editor extension, Cursor chat or desktop app it came from, or a cloud session in its browser or app. For an editor extension, the server first opens the conversation's folder in that editor, so the link reaches the window that holds it.
Codex and Cursor stores are read with the sqlite3 command.
Each row has a state, the first of these that holds:
blocked: open, and its tool says it waits for you.working: open, and its tool says the agent is busy.completed: you marked it completed and nothing was written since.unread: written after you last saw it.active: open, or written in the last 24 hours.dormant: closed and quiet for longer.
The activity of a loaded local Codex thread is working during an unfinished turn. It is inferred to be waiting after five minutes without a rollout write, which makes the state blocked until you see it. A long-running command can also meet that threshold.
Marks:
seenAtandcompletedAtare your marks, when set.needsYoucounts the blocked and unread conversations of every age and tool the server lists, whatever the page or the search.- The marks live in
conversation-marks.jsonin theagnostic-aifolder of your user configuration folder. The file holds tool names, ids and times only. - The first read starts the file, and what was written before it counts as seen.
- A read deletes a completion that a later message overtook. That conversation is then unread.
marksNotesays why the file could not be read or written. Rows then carry no marks.
Cloud sessions and refresh:
- On a server started with
--cloud-sessions, Claude Code cloud sessions and Codex cloud threads are listed too, withappcloud.cloudNotesays why they are missing when they could not be read. - Codex and Cursor chats are read again only when their files change, for at most a minute. One of them can show as open for up to a minute after its app quit.
- The reply has an
ETag. It changes when a chat or a mark changed, when a new minute began (a process that ended or a chat that went dormant shows no other way), or when the query differs. - Sent back as
If-None-Match, an unchanged list answers 304 with no body and reads no chat.
POST /api/conversations/open
Body: {tool, id, editor?} with Content-Type: application/json, from the server's own origin.
- Reopens a conversation the list holds along its
openroute and returns{opened, message, command?}. openedis false when it could not: a live session in another app's terminal, a system that cannot open Terminal, or macOS refusing Automation access.commandthen resumes it by hand.- The route comes from the server's own scan, never from the request.
- A conversation outside the list returns 404.
- Any other field is a 400.
- Opening Terminal asks once for macOS Automation access.
- A conversation that opened is marked seen, and a completion on it is taken back.
POST /api/conversations/mark
Body: {tool, id, as} with Content-Type: application/json, from the server's own origin.
as is one of:
seen: read up to now.completed: done until something new is written. This also marks it seen.open: takes a completion back.hidden: left out of every list, search and count until something new is written. The tool's own store keeps it. This also marks it seen.
Replies:
- Returns 204.
- A conversation outside the list returns 404.
- Another
as, or any other field, is a 400.
GET /api/views/authority
Query: ?agentId=<id>&target=<tool> for per-agent decisions.
POST /api/views/authority/impact
Body: a permission proposal to preview.
GET /api/views/authority/schema
Takes no input.
POST /api/edit/builtin
Body: {audience, name, enabled, expected?}, or {audience: "personal", reset: true, expected?} to drop the personal list.
expectedis the shown boolean, or the shown list for reset. A changed value returns 409 without saving the requested change.- Built-ins apply to every tool, so a
targetother thanallreturns 400.
POST /api/edit/targets
Body: {audience, name, enabled, expected?}, or {audience: "personal", reset: true, expected?} to drop the personal list.
nameis a built-in coding tool. With--global, it is onesync --globalcan write.expectedis the shown boolean, or the shown list of tools that are on for reset. A changed value returns 409 without saving the requested change.- Turning off the last tool returns 400.
- Returns
{ok, file}.
POST /api/edit/value
Body: {audience, path, key, value, expected?}. value is a boolean, or null with audience: personal to reset.
expectedis the shown boolean. A changed value returns 409 without saving the requested change.- The key already names the tool (
x-claude.verbose), so atargetother thanallreturns 400.
POST /api/edit/mcp
Body: {action, audience, name, target, path, scope, expected?}.
name,target,path, andscopeidentify one native row from the overview. Herescopeis its source scope (projectoruser).action: "preview"returnsfile,spec,variables,targets,syncCommand, andexpected.action: "apply"requires thatexpected, writes only the spec, and returns those fields withok: true.- A changed preview or existing spec stops apply.
- A credential that import cannot rewrite is refused with its import message.
- Personal saves use the same Git-ignore check as other edits.
POST /api/edit/hook
Body: {action, audience, name, on, match?, command, timeout?, description?, expected?}.
nameuses lowercase letters, digits, and hyphens.onis a portable event.match(shell,edit,read, orweb) goes only withbefore-toolandafter-tool.timeoutis a whole number of seconds above zero.action: "preview"returnsfile,spec,syncCommand, andexpected. It answers 422 withproblemswhenvalidatewould report the hook.action: "apply"requires thatexpected, writes only the new hook file, and returns those fields withok: true.- An existing hook name returns 409. So does a changed preview.
- Local saves (
audience: personal) use the Git-ignore check.
POST /api/edit/spec/move
Body: {action, kind, name, from, to, mode, overwrite?, expected?}. Copies or moves one shared spec between the project and the global setup.
kindisrule,agent,skill,hook, ormcp, since the global setup reads no other kind.fromis the level this server reads andtois the other.modeiscopyormove.- A skill folder moves whole.
action: "preview"returnssource,destination,exists,files,syncCommands, andexpected.action: "apply"requires thatexpected, which holds for eithermodeandoverwrite. It writes the destination, removes the source formove, and returns those fields withok: true, pluswarningwhen a leftover folder needs deleting by hand.
These return 409:
- An existing destination, unless
overwriteistrue, which replaces it whole. - A spec of that name at another path.
- A changed preview.
- A destination that is the source itself (one setup's folder links to the other's).
- A skill the destination would read differently (under another skill's folder, or over a folder of other skills).
- A project server whose global setup does not exist yet. The reply carries
missing: trueandroot. - A global server that knows no project.
These return 422:
- A built-in, a pack spec, a local spec, a
SKILL.mddirectly in the skills folder, or a destination folder that links outside its setup. - A rule sent to the global setup with a scope, paths, or target condition, including one under an
x-<target>key for a target the global setup selects.
POST /api/edit/permissions
Body: {action, audience, path?, key, rules, before?, expected?}.
keyispermissions.allow,permissions.ask, orpermissions.deny.rulesis a string list, ornullto remove that list.beforeis the selected audience's own list as shown, ornullwhen absent. A mismatch returns 409.- Preview returns
schemaVersion: "permission-edit/v1",file,key,audience,before,rules,diff,expected, andsyncCommand. - Apply requires the preview's
expected, rechecks its inputs, saves only the source file, and returns the preview fields withok: true. - Personal saves use the Git-ignore check.
POST /api/edit/protected
Body: {action, audience, path?, protected, before?, expected?}.
protectedis{paths, decision, reason}, wheredecisionisaskordeny, ornullto remove the block.- It writes the block to the settings spec the permission lists use, replacing any block there.
- A local save writes only the keys that differ from the project block, so a later project change to the others still applies. It returns 400 when it would lower a project
denytoask. beforeis the selected audience's own block as shown, ornull. A mismatch returns 409.- Preview returns
schemaVersion: "protected-edit/v1",file,audience,before,protected,diff,expected, andsyncCommand, pluscleared: truewhen the file setsprotected: null, whichprotected: nullin the request removes. - Apply works as for
/api/edit/permissions. - The global server returns 400, because
sync --globaldoes not write protected paths.
POST /api/edit/field
Body: {audience, path?, key, target?, value, expected?} for model, effort, and global permissions.default-mode, session-retention-days, voice.enabled, voice.mode, or voice.language.
targetisall(the default) or a configured tool.- For one tool, the edit writes
model.<tool>oreffort.<tool>, turningmodel: xinto{default: x, <tool>: value}. An effort level is checked against that tool only. - A tool with no effort setting returns 400.
- When a model tier gives the spec its effort, one tool's effort returns 422, because a spec value turns the tier's effort off for every tool.
- Session retention takes only
all. - With
all, a field that is already a per-tool map gets itsdefaultentry. nullclears the value at that address, or resets your override withaudience: personal.- A map left with only
defaultgoes back to a single value. The exception is a personal file over a shared per-tool map. There a single value would replace the whole shared map, so it stays{default: x}. expectedis the value the page showed at that address in the file being written, ornullif that file did not set it.- If the file now holds another value, or a personal file now sets that address for a shared save, the answer is 409 and nothing is written.
- A file that already holds the new value counts as saved.
- Voice values are a boolean for
voice.enabled,holdortapforvoice.mode, and a non-empty string forvoice.language. Only the global view and targetallare accepted. - For voice,
nullclears one member, removing an emptyvoicemap.
GET /api/changes/builtin
Takes no input. Returns the session's change state.
POST /api/changes/builtin/preview
Body, one of:
{audience, name, enabled}for any shipped built-in.{audience: "personal", reset: true}.{audience, setting: {path?, key, target?, value, expected?}}formodeloreffort, with the fields/api/edit/fieldtakes.
A setting plan carries key, target for one tool, and a title such as effort set to high or model for codex set to sol. Undo state names it in undoTitle.
POST /api/changes/builtin/apply, .../undo, .../cancel
Body: {id} from the change state.
- Cancel discards that preview without writing files and keeps the last undo.
- Cancel answers 409 when that preview was already replaced or closed.
When path is omitted
Omit path only when at most one editable settings spec exists. With none, a field save creates settings/defaults.yaml, or a permission save creates settings/permissions.yaml, for the selected audience. The response names the edited file.