CLI Control (control Omniscio from scripts and hotkeys) (part 2)
Part 2 of the CLI Control page: the endpoints an outside script or AI uses to change things on your behalf — driving a running session, registering, editing or deleting a project, adding and removing project docs, away-mode rules, and firing a recipe — plus how the approval queue and the rate caps treat each one.
What it is
This is part 2 of the CLI Control (control Omniscio from scripts and hotkeys) page. It covers the endpoints an outside script or AI uses to change things on your behalf — driving a session that is already running, registering, reordering, editing or deleting a project, adding and removing files in a project's docs folder, away-mode rules, and firing a recipe run.
Beyond cron, automations, settings, sessions, recipes, and keybindings, the same server hosts three more surfaces that an external AI (or a script) can drive on the user's behalf. Two of them (project DELETE, every away-mode mutation, recipe run) land as pending rows in the same cli_pending_actions queue that gates settings PATCH (and session lifecycle only when its approval toggle is on — by default lifecycle applies immediately) — they show up in Omniscio's inbox as approve/reject cards and only have a side effect once the user clicks Approve. Cosmetic project edits (rename, recolor, pin, divider, reorder) are NOT gated because they're personal preferences that the user can revert without consequence.
When several approval cards pile up (e.g. an agent queues many project.docs_upload rows at once), they don't have to be approved one at a time: the Approvals inbox section header shows an "Approve all N" button (one confirm, then it approves the whole batch), and an arbitrary subset can be multi-selected and approved together (mobile selection bar / desktop right-click "Approve N selected"). Bulk approval is exactly N single approves — each card still runs its OWN approve-time re-validation independently (a tampered row is rejected on its own while the rest go through), so it never weakens the two-gate check described below. Two cards are never part of a bulk gesture: the delete-old-branches card (worktree.stale_branch_retire), whose approval retires every branch in its batch, and the auto-lander's preservation-override card (autolander.preservation_overlap_review), whose approval lets a branch its safety check refused land. "Approve all", "Reject all" and the selection actions all skip them and leave them waiting, so each is only ever decided on its own card.
Where to find it
Same surface as the parent page: every endpoint here is served by the same local address, and the token you copy from Settings → CLI Control is the one they all take. The ones that change something you would have to live with — deleting a project, uploading or removing a docs file, editing an away-mode rule, running a recipe — do not act the moment they arrive: they queue an approval card in your inbox, and the family-wide switches for that are in Settings → CLI Control → Approval requirements.
How it behaves
Session actions — /session/*
Drive an existing session directly. All four apply immediately and are IDOR-bound — the regular ~/.amc/cli-token acts on any session; a minted in-app-session token may act only on its own session. They join the existing lifecycle (pause / unpause / snooze / archive), recovery (restart / nudge / move-account), and rename routes on the same server.
POST /session/:id/message(auth,200 OK) — billable. Send the user's next turn to a session. Body{ text }. Unlike/nudgeand/peer-message(which mark the turn Omniscio-injected), this is a real user message. Spawns / resumes the session as needed;409if the session is paused (unpause it first), and409 inactive_target_needs_confirmationif it is archived — re-send with"confirmInactiveTarget": trueonly when it genuinely matters (owner rule 2026-09-24). If the target is mid-turn, this kills and respawns it immediately — no confirmation flag, unlikepeer-message's opt-inconfirmInterruptTurn(see peer-message-interrupt-and-wake.md). Shares the 120/hr peer-message bucket; anX-Client-Request-Idheader makes a retry safe.POST /session/:id/move(auth,200 OK) — move a session to another project. Body{ targetProjectId }. A started session keeps its working directory (pinned so--resumestill resolves); sentinel / tool-panel targets are rejected (400), and a same-project move is409. Free.POST /session/:id/schedule-response(auth,200 OK) — billable at delivery. "Send Later": queue the user's next turn for a future time. Body{ text, sendAt (strict ISO-8601, ≥ ~30 s out), force? }(text-only).409if one is already scheduled unlessforce:true(the body then carries the existing text + time). Scheduling is free; the eventual delivery is the paid turn.POST /session/:id/interrupt(auth,200 OK) — stop the current turn (kills the child process) while keeping the session alive and re-sendable — distinct from pause (a status) and archive (a terminal close).409if the session isn't running. Free.
Full request/response shapes + curl examples live on the omniscio-control sessions surface — see omniscio-control.md. Because /message and /schedule-response spend money (a real AI turn), an agent calls them only when the user explicitly asks.
Project management — /project/*
POST /project/create(auth,201 Created) — register an existing folder on disk as a new Omniscio project. Body{ name, folderPath, color?, dividerId? }. The folder must already exist; the route returns 400 otherwise. EmitsPROJECTS_CHANGED. Persists immediately, no inbox round-trip — there's no destructive side effect to gate.POST /project/reorder(auth,200 OK) — bulk-update sidebar display order. Body{ orderedIds: [<projectId>, ...] }— each id a non-empty string (not necessarily a UUID), up to 1000; an empty array validates and is a no-op. EmitsPROJECTS_CHANGED. Cosmetic, immediate.PATCH /project/:id(auth,200 OK) — apply cosmetic edits. Optional fields:name,color(nullable — passnullto clear the project tint, mirrors the EditProjectDialog "No color" button),hideBranchInHeader,isPinned,dividerId(nullable — passnullto remove the project from its current group),defaultProvider(a pickable provider id — e.g."claude","codex","gemini","grok","gpt"— ornull— sets this project's default AI provider override, mirrors the EditProjectDialog provider radios. The route validates against the provider registry's full pickable set (PICKABLE_PROVIDER_ID_VALUES= every provider the new-session picker offers), which grows automatically as new pickable providers ship — so the accepted set is exactly whatever is currently pickable, not a fixed list (don't hard-code a count against this: it has drifted 9 → 11 → 15 → 19 → 20 as providers shipped);nullclears the override and falls back to Claude). Two more optional fields mutate project STATE (beyond the cosmetic set above):isolationEnabled(boolean — toggles this project's git-worktree isolation, applied immediately viasetProjectIsolation) anddeployProfileId(a deploy-profile id string, ornullto clear the project's deploy-profile association, applied viasetProjectDeployProfile). One more optional field:cloudMachineType— the SIZE of cloud machine this project launches. One of"small","default","large","xlarge"(the vendor's own four sizes), ornullto clear back to the vendor default. The absent-vs-null distinction is the same onecolorcarries: omitting the key leaves the stored size untouched, while an explicitnullclears it — so a caller that sends only{ "name": … }cannot accidentally resize a project."default"is stored as itself, NOT as null, even though both launch identically (neither passes--typeto the provider), because the row has to keep saying a person CHOSE the standard size. The value reaches the launch as the provider's--type;"default"andnullproduce byte-identical commands by design. Send only the fields you want to change. EmitsPROJECTS_CHANGED. Cosmetic, immediate.defaultProviderwrites through the globalprojectDefaultProvidersRecord setting and triggersSETTINGS_CHANGEDas well so the EditProjectDialog radios + sidebar "!" cue update without an app refresh.TOKEN=$(cat ~/.amc/cli-token) # Set this project's default provider to Codex curl -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"defaultProvider":"codex"}' \ http://127.0.0.1:19519/project/<projectUuid> # Clear the override (fall back to Claude) curl -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"defaultProvider":null}' \ http://127.0.0.1:19519/project/<projectUuid>DELETE /project/:id(auth,202 Accepted) — approval-gated. Queues aproject.deleterow incli_pending_actionsand emitsCLI_PENDING_CHANGED. The actual delete (which cascades to sessions) doesn't happen until the user approves in the inbox. Idempotency on theX-Client-Request-Idrequest header (DELETE bodies are spec-discouraged) — same id within 30 days returns the existing pending row withidempotent: true.PATCH /project/:id/bug-intake(auth,202 Accepted) — approval-gated. Queues aproject.bug_intake_updaterow that enables, changes, or disables the project's bug-report intake slug. Body{ enabled: true, slug: "<a-z0-9-, 1-64>" }to enable/change, or{ enabled: false }to disable. Slug uniqueness is enforced case-insensitively against other non-deleted projects (409 at queue and apply time). Approval-gated because flipping intake on opens an external email side channel and auto-spawns Claude sessions on incoming reports — see bug-report-intake.md. Idempotency viaX-Client-Request-Idheader.
Project docs (.claude/docs/) — /project/:id/docs
Lets an external agent inspect, add, and remove files in a project's .claude/docs/ folder — the per-project knowledge folder that gets auto-injected into every session in that project (see project-docs-auto-injection.md for what the folder does). Mutations are approval-gated; the read endpoint is open to authenticated callers.
GET /project/:id/docs(auth,200 OK) — list current files in the project's.claude/docs/. Response:{ ok: true, files: [{ name, sizeBytes, ext, modifiedAt }, ...], ragFiles: [ ...same entry shape... ], totalBytes, ragTotalBytes, quotaBytes, truncated }. Docs live in TWO buckets:files/totalBytesare the always bucket (top-level.claude/docs/, auto-injected into the first message of every session, counted againstquotaBytes);ragFiles/ragTotalBytesare the rag bucket (.claude/docs/rag/, surfaced as an on-demand table-of-contents the agent reads selectively and NOT counted against the always-bucketquotaBytes).extis the lower-cased extension including the leading dot (e.g..md);truncatedflips totrueonce 1000 files are listed (older entries are dropped). Returns 404 for unknown project, 401 without bearer token, 429 when the per-bearer read budget (60/min) is exhausted.POST /project/:id/docs(auth,202 Accepted) — approval-gated. Queues aproject.docs_uploadrow. Body{ sourcePath: "<absolute path to a regular file>", destFilename: "<safe filename>", bucket?: "always" | "rag" }(bucketdefaults to"always"; pass"rag"to route the file into the uncapped.claude/docs/rag/bucket instead of the always-bucket) — the agent writes the file to a%TEMP%/scratch directory FIRST and passes its absolute path; Omniscio copies it into.claude/docs/<destFilename>only after approval. Validation at submit time: filename rules (ASCII printable, single extension, no path separators, not a Windows reserved device likecon.txt/PRN.log), source must be a regular file (symlinks rejected — security), source ≤ 25 MB per file, project total ≤ 100 MB after the add, destination must not already exist. Returns 400 for filename / source / size violations, 409 fordest already existsor pending-action cap, 413 for size/quota, 404 for unknown project. Idempotency viaX-Client-Request-Idheader.DELETE /project/:id/docs/:filename(auth,202 Accepted) — approval-gated. Queues aproject.docs_deleterow. Accepts an optional?bucket=always|ragquery param (defaultalways) selecting which bucket to delete from — an unknown value returns 400. The filename must pass the samevalidateFilenamerules; the file must exist at submit time (returns 404 otherwise so the caller doesn't queue an approval for a no-op). Idempotency viaX-Client-Request-Idheader.
The two-gate validation matters: filename rules and existence checks run at BOTH submit time (so a misshapen request fast-fails with 400/404 instead of polluting the inbox) AND approve time (so a tampered queue row can't sneak ..\..\evil.dat past). Path-based upload (sourcePath) is deliberate — base64 in the request body would force the agent to load the entire file into memory and would balloon the JSON payload past most rate-limit / log-line thresholds, while a path-based handoff is constant-cost regardless of file size.
TOKEN=$(cat ~/.amc/cli-token)
# List
curl -H "Authorization: Bearer $TOKEN" \
http://127.0.0.1:19519/project/<projectUuid>/docs
# Upload (queue approval)
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"sourcePath":"/tmp/notes.md","destFilename":"notes.md"}' \
http://127.0.0.1:19519/project/<projectUuid>/docs
# Delete (queue approval)
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
http://127.0.0.1:19519/project/<projectUuid>/docs/notes.md
Away-mode rules — /away-mode/rules
GET /away-mode/rules(auth,200 OK) — list all away-mode rules. Auth-gated despite being read-only — returns 401 without the bearer token.POST /away-mode/rules(auth,202 Accepted) — approval-gated. Queues anaway_mode.createrow. Body{ conditionType: "keyword" | "project" | "tag" | "time", conditionValue, responseText, snippetId? }. EmitsCLI_PENDING_CHANGED.PATCH /away-mode/rules/:id(auth,202 Accepted) — approval-gated. Queues anaway_mode.updaterow. Same body fields as POST (all optional). The route 404s if the rule no longer exists, so dispatchers don't waste approvals on dead targets.DELETE /away-mode/rules/:id(auth,202 Accepted) — approval-gated. Queues anaway_mode.deleterow. No body. Same 404 fast-fail as PATCH.
All three away-mode mutations accept X-Client-Request-Id (header, ≤64 chars) for idempotent retries; same id within 30 days returns the existing pending row.
Recipe runs — /recipe/configs/:id/run
POST /recipe/configs/:id/run(auth,202 Accepted) — approval-gated. Queues arecipe.runrow incli_pending_actions. Body fields:customMessage?(≤10 000 chars; overrides the recipe's default kickoff prompt) andvariables?(object of string-keyed values that fill the recipe's parameter slots). Returns 400 if the recipe lacks ahomeProjectId(CLI v1 doesn't run virtual-project recipes), 404 if the id is unknown. EmitsCLI_PENDING_CHANGED. Idempotency viaX-Client-Request-Idheader.
The recipe also needs approvalStatus: "approved" for the eventual run to actually start — but this endpoint enqueues the run regardless. The recipe engine itself refuses to spawn for an unapproved recipe; the user approves the recipe once (in the inbox) before the first run will actually fire.
Approvals, caps and rate limits
All approval-gated mutations above share the same cap on outstanding pending actions and the same 10/min rate-limit bucket as cron, automations, settings PATCH, session lifecycle, keybindings, and recipe authoring. A 409 means either the cap is full (list GET /cli-pending?status=pending to see what's queued) or — for POST /project/create only — the folder is already registered as a project.
Related
The endpoints that watch and drive what is on your screen are in part 3, and how to switch CLI Control on and call it is on the parent page. Firing a recipe from an agent has its own page in agent-trigger-recipes.md, and the folder this part uploads into is explained in project-docs-auto-injection.md.
Last verified 2026-09-28