---
title: CLI Control (control Omniscio from scripts and hotkeys) (part 2)
---

# CLI Control (control Omniscio from scripts and hotkeys) (part 2)

## What it is

This is part 2 of the [CLI Control (control Omniscio from scripts and hotkeys)](cli-control.md) 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](cli-control.md): 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 `/nudge` and `/peer-message` (which mark the turn Omniscio-injected), this is a real user message. Spawns / resumes the session as needed; **`409`** if the session is paused (unpause it first), and **`409 inactive_target_needs_confirmation`** if it is **archived** — re-send with `"confirmInactiveTarget": true` only when it genuinely matters (owner rule 2026-09-24). **If the target is mid-turn, this kills and respawns it immediately — no confirmation flag, unlike `peer-message`'s opt-in `confirmInterruptTurn`** (see [peer-message-interrupt-and-wake.md](peer-message-interrupt-and-wake.md)). Shares the **120/hr** peer-message bucket; an `X-Client-Request-Id` header 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 `--resume` still resolves); sentinel / tool-panel targets are rejected (**`400`**), and a same-project move is **`409`**. 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). **`409`** if one is already scheduled unless `force: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). **`409`** if the session isn't running. Free.

Full request/response shapes + `curl` examples live on the omniscio-control **sessions** surface — see [omniscio-control.md](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. Emits `PROJECTS_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. Emits `PROJECTS_CHANGED`. Cosmetic, immediate.
- `PATCH /project/:id` (auth, **`200 OK`**) — apply cosmetic edits. Optional fields: `name`, `color` (nullable — pass `null` to clear the project tint, mirrors the EditProjectDialog "No color" button), `hideBranchInHeader`, `isPinned`, `dividerId` (nullable — pass `null` to remove the project from its current group), `defaultProvider` (a pickable provider id — e.g. `"claude"`, `"codex"`, `"gemini"`, `"grok"`, `"gpt"` — or `null` — 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); `null` clears 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 via `setProjectIsolation`) and `deployProfileId` (a deploy-profile id string, or `null` to clear the project's deploy-profile association, applied via `setProjectDeployProfile`). 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), or `null` to clear back to the vendor default. The **absent-vs-null distinction is the same one `color` carries**: omitting the key leaves the stored size untouched, while an explicit `null` clears 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 `--type` to 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"` and `null` produce byte-identical commands by design. Send only the fields you want to change. Emits `PROJECTS_CHANGED`. Cosmetic, immediate. `defaultProvider` writes through the global `projectDefaultProviders` Record setting and triggers `SETTINGS_CHANGED` as well so the EditProjectDialog radios + sidebar "!" cue update without an app refresh.

  ```bash
  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 a `project.delete` row in `cli_pending_actions` and emits `CLI_PENDING_CHANGED`. The actual delete (which cascades to sessions) doesn't happen until the user approves in the inbox. Idempotency on the `X-Client-Request-Id` request header (DELETE bodies are spec-discouraged) — same id within 30 days returns the existing pending row with `idempotent: true`.
- `PATCH /project/:id/bug-intake` (auth, **`202 Accepted`**) — **approval-gated**. Queues a `project.bug_intake_update` row 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](bug-report-intake.md). Idempotency via `X-Client-Request-Id` header.

### 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](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`/`totalBytes` are the **always** bucket (top-level `.claude/docs/`, auto-injected into the first message of every session, counted against `quotaBytes`); `ragFiles`/`ragTotalBytes` are the **rag** bucket (`.claude/docs/rag/`, surfaced as an on-demand table-of-contents the agent reads selectively and NOT counted against the always-bucket `quotaBytes`). `ext` is the lower-cased extension including the leading dot (e.g. `.md`); `truncated` flips to `true` once 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 a `project.docs_upload` row. Body `{ sourcePath: "<absolute path to a regular file>", destFilename: "<safe filename>", bucket?: "always" | "rag" }` (`bucket` defaults 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 like `con.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 for `dest already exists` or pending-action cap, 413 for size/quota, 404 for unknown project. Idempotency via `X-Client-Request-Id` header.
- `DELETE /project/:id/docs/:filename` (auth, **`202 Accepted`**) — **approval-gated**. Queues a `project.docs_delete` row. Accepts an optional `?bucket=always|rag` query param (default `always`) selecting which bucket to delete from — an unknown value returns 400. The filename must pass the same `validateFilename` rules; the file must exist at submit time (returns 404 otherwise so the caller doesn't queue an approval for a no-op). Idempotency via `X-Client-Request-Id` header.

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.

```bash
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 an `away_mode.create` row. Body `{ conditionType: "keyword" | "project" | "tag" | "time", conditionValue, responseText, snippetId? }`. Emits `CLI_PENDING_CHANGED`.
- `PATCH /away-mode/rules/:id` (auth, **`202 Accepted`**) — **approval-gated**. Queues an `away_mode.update` row. 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 an `away_mode.delete` row. 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 a `recipe.run` row in `cli_pending_actions`. Body fields: `customMessage?` (≤10 000 chars; overrides the recipe's default kickoff prompt) and `variables?` (object of string-keyed values that fill the recipe's parameter slots). Returns 400 if the recipe lacks a `homeProjectId` (CLI v1 doesn't run virtual-project recipes), 404 if the id is unknown. Emits `CLI_PENDING_CHANGED`. Idempotency via `X-Client-Request-Id` header.

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](cli-control-part-3.md), and how to switch CLI Control on and call it is on the [parent page](cli-control.md). Firing a recipe from an agent has its own page in [agent-trigger-recipes.md](agent-trigger-recipes.md), and the folder this part uploads into is explained in [project-docs-auto-injection.md](project-docs-auto-injection.md).
