---
title: CLI Control (control Omniscio from scripts and hotkeys)
---

# CLI Control (control Omniscio from scripts and hotkeys)

## What it is

CLI Control is a tiny HTTP server that Omniscio runs on your own machine at **`127.0.0.1:19519`**. It lets any tool that can make an HTTP request — AutoHotKey, PowerShell, curl, Python, Bash, an external AI like Claude Code — navigate Omniscio, launch sessions with a pre-typed prompt, bring the window to front, pop a project or integration out into its own window, or create/schedule/toggle cron jobs. It's on by default, localhost-only (cannot be reached from other machines), and any mutation (starting a session, creating a cron job) requires a **token** that Omniscio auto-generates and stores at `~/.amc/cli-token` with OS-level file-permission hardening. Read-only endpoints like `/ping` and `/status` are open, so your scripts can check health without handling the token.

## Where to find it

CLI Control lives in **Settings → CLI Control**, which is where you switch the server on or off, read and copy its token, regenerate that token, and decide which outside requests must wait for your approval. It is not a panel you open to use; it is a door other programs knock on, and that Settings page is where you choose how far it opens.

## How it behaves

### How to use it

1. **Confirm it's on and copy your token.** Open Settings → **CLI Control**. CLI Control ships enabled. The **Token** field shows a hex string you can copy; use **Regenerate** to invalidate the old one at any time. A small "Created N ago" line under the token shows its age and turns amber once it is over 90 days old, nudging you to Regenerate (the token itself never auto-expires). A fresh token is also mirrored to `~/.amc/cli-token` (`%USERPROFILE%\.amc\cli-token` on Windows).
2. **Call the read-only endpoints.** `GET http://127.0.0.1:19519/ping` returns `{ok: true}`. `GET /status` lists your projects and integrations. `GET /cron/jobs`, `/cron/jobs/:id`, `/cron/jobs/:id/runs`, `/cron/runs/upcoming` return cron state — these require `Authorization: Bearer <token>` (they surface private cron data) and share a per-token 60/min read budget. No token is needed for `/ping` and `/status`.
3. **Navigate or focus.** `GET /inbox`, `/focus`, `/project/<name>` (fuzzy-matched, case-insensitive), or `/superprompt/<id>`. As of the CLI-server hardening these need a **valid bearer token** (`Authorization: Bearer <token>`) **or** a same-origin request from Omniscio's own UI — a header-less, tokenless call is now rejected with `403`, closing a hole where any local program (or a malicious web page) could steal window focus or force navigation without your token. Hotkey macros must send the `Authorization` header. (`/ping` and `/status` stay open with no token.)
4. **Launch a session or create a cron.** `GET /project/<name>/new?token=<YOUR_TOKEN>&prompt=<url-encoded prompt>` spawns a session and auto-sends the prompt. The spawn is created by the background engine and is **durable** — as of 2026-06-04 it no longer depends on the Omniscio window being responsive, so a `200` means the session was actually created, and a spawn survives a busy/frozen UI or an app restart mid-spawn (a rapid burst is paced, returning `202` for the queued ones). **By default the new session opens silently in the background — Omniscio does NOT come to the foreground.** That's the right default for agent-driven calls (the user is working in another app and shouldn't be yanked away). Hotkey-driven workflows that DO want focus must opt in with `&focus=true` (only the literal string `true` opts in; `focus=yes` / `focus=1` are ignored). `/focus` and `/window/open` bring the window forward; `/project/<name>` (activate-project, no `/new`), `/inbox`, `/settings/open`, and the note / mind-map / Gmail-thread / saved-prompt routes SWITCH which screen you're looking at — and the "don't take over the user's screen" rule covers both. **(a) Foreground:** an **agent-initiated** `/focus` / `/window/open` (a call sending `X-AMC-Source-Session-Id`) is **auto-blocked while a different app owns the foreground** (returns `focusSuppressed: true` + an inbox note you can silence via its "Turn off these notices" button or Settings → Notifications → "Screen-grab block notices"); it still foregrounds when you're already in Omniscio, or when you opt in at Settings → CLI Control → "Let agents bring Omniscio to the front" (default off). **(b) View-switch:** an agent-initiated activate-project / `/inbox` / setting / note / mind-map / Gmail-thread / saved-prompt request no longer changes your view directly — while you're IN Omniscio it drops a **click-to-go toast** ("…wants to switch to X — Go there") that navigates only when you click, and while you're in another app it drops the same inbox note; either way it never moves you on its own, unless you opt in at Settings → CLI Control → "Let agents switch your view directly" (default off). A human's own hotkey, notification click, phone tap, or the in-app UI is never affected. `/cron/jobs` supports `POST` (create), `PATCH /cron/jobs/:id` (update), `DELETE /cron/jobs/:id` (delete), `POST /cron/jobs/:id/run` (fire now), and `POST /cron/jobs/:id/toggle` (enable/disable). All cron mutations require `Authorization: Bearer <token>` and are capped at **10 per minute**. There is **no ceiling on how many cron jobs an install may hold** — the `MAX_CRON_JOBS` row-count cap (200, later 500) was removed on 2026-09-06 because it measured the wrong quantity: it counted every row in the table (inactive, rejected, spent one-shots, wake schedules), so an install with 199 dead rows was refused a create while one with 199 jobs firing every five minutes was not. What replaced it is the create-RATE policy above, which is what a runaway caller actually runs into; the single source is [cron-job-create-rate.ts](/src/shared/cron-job-create-rate.ts).
5. **Pop a project or integration into its own window.** `POST /window/open` with a JSON body `{ "projectId": "<id>" }` opens (or focuses) that project — a real project or an integration like Tasks — in its own themed desktop window, the HTTP half of the in-app "Open in new window" button. `projectId` takes a project UUID, a friendly alias, or an integration id such as `tasks-v2`. It's bearer-authed, shares the 10/min mutation cap, and is idempotent (a second call just focuses the existing window, returning `state: "already-open"`). Full details — id resolution, response shape, and the `404` / `409` (a dedicated window like KMS has its own route) / `400` (no panel to show) cases — live on the omniscio-control **windows** surface. (Yes, this is supported: earlier notes that called a CLI pop-out "out of scope" predate this route.)
   - **POST variant for Unicode-safe prompts.** `POST /project/<name>/new` exists alongside the GET form because Windows mangles non-ASCII chars (em-dash, arrows, accented chars, emoji, non-Latin scripts) in the cmd.exe / Git Bash argv pipeline before curl can encode them — they arrive at the spawned session as `?` or U+FFFD replacement chars. The POST variant takes the prompt as a JSON body field, which streams from the socket as raw bytes and bypasses argv encoding entirely. Auth is **header-only** (`Authorization: Bearer <token>` — query-string `?token=...` is rejected with 401), Content-Type must be `application/json` (charset suffix allowed), body schema is `{ prompt?: string, name?: string, provider?: ProviderId, focus?: boolean }` (`.strict()`). Empty body is allowed for a source-less call (no prompt → blank session), but a prompt-less **agent** spawn — one sending `X-AMC-Source-Session-Id` — is rejected `400` ("a spawned session needs a prompt"): an agent always has a task for the child it spawns, so a missing prompt is a caller bug that would otherwise leave a dead empty session. Only literal boolean `true` opts into focus. **`provider`** forces the engine for this one session; **omit it and the session uses the project's configured default provider** (the one set via `PATCH /project/:id` `defaultProvider`, gated by the show-alternate-providers master toggle), falling back to Claude — the same provider the UI's **+ New** button would pick, so a CLI / cron / recipe spawn into a project defaulted to e.g. Gemini now correctly starts on Gemini instead of silently using Claude. **`name`** stamps an exact session title instead of letting the AI titler infer one. The 1 MB body cap matches the other mutations, but `/new` is deliberately **not** on the 10/min mutation bucket — spawns are serialized by a FIFO spawn pacer at one every 30 s (≈2/min), and a burst is queued and released in order (each queued spawn returns `202`) up to a 50-deep runaway backstop that returns `429`. Validation errors are self-explanatory: a malformed JSON body returns `400` with the parser’s own message, the character position, and a short snippet of the body around the break (echoed only to the caller, never logged), and an unrecognized body field returns `400` with a did-you-mean hint (`Unexpected field: message — Did you mean ‘prompt’?` — common aliases like `message`/`text`/`task` map to `prompt`, and close typos suggest the nearest accepted field). An optional **`X-Client-Request-Id`** header (≤64 chars) makes a retry safe — reuse the same id and a duplicate POST returns `200 { idempotent: true }` instead of spawning a second session (without it, `/new` does not dedup). That replay is a **receipt, not a bare acknowledgement**: it carries the ORIGINAL spawn's `spawnId`, plus `status` and `sessionId` once the session exists — so a caller that timed out and retried learns WHICH session its first call made instead of being told only that something happened. **Source required by default:** with `requireSpawnSourceSession` on (the default), both `/new` forms AND `POST /agent/sessions` refuse a spawn that sends no source with `400` — pass `X-AMC-Source-Session-Id: $AMC_SESSION_ID` (an Omniscio-spawned agent has it in env) so the child links back to its origin, or the user turns the setting off in Settings → CLI Control for anonymous script/hotkey spawns. **A scoped agent token can't spawn by default:** the `/new` forms accept the global CLI token and an in-app session, but an agent's _scoped_ per-session token (the `$AMC_CLI_TOKEN` Omniscio injects into every spawned session) is refused (`401`/`403`) unless the user opts in at Settings → CLI Control → "Let agents spawn sessions with their own token" (`allowAgentTokenSpawn`, default off) — so a compromised MCP server / npm dep reading an agent's env can't spend money by spawning paid sessions; the narrowest per-MCP-server token never spawns. (The separate `POST /agent/sessions` agent-driven-spawn path has its own `agentDrivenSessionsEnabled` gate.)
   - **Confirming a spawn — `GET /spawn/:spawnId`.** Every `/new` reply (200 immediate, 202 queued, 202 starting) carries a durable `spawnId`; this route resolves it. Response `{ ok: true, spawnId, status, sessionId, error }` where `status` is `created` (the session exists — `sessionId` names it), `pending` (accepted, not driven yet), `failed` (the driver gave up, or its caller withdrew it — `error` says why), or `unknown` (no spawn by that id). Resolved from the session's own `origin_spawn_id` anchor, so it is exact. **Confirm with this, never by listing `/sessions` and taking the newest** — a `perf:instant-new-session` prewarm shell is indistinguishable from a fresh spawn (both blank + `ready`), so a timestamp guess picks the wrong one. Read-only, on the 60/min read budget; the global CLI token sees any spawn, an agent's scoped token only spawns it originated (`403` otherwise).
   - **Withdrawing a queued spawn — `DELETE /spawn/:spawnId`.** A spawn paced into the queue (`202`) can be taken back before it starts. The spawn pacer gives each caller a share of its 50-deep queue measured from the queue itself, so a request you no longer want holds a slot you cannot use — and until this route there was no way to give one back. Withdrawing cancels the request itself, not just its place in line, so nothing can start it later — `GET /spawn/:spawnId` then answers `failed` with the `error` "withdrawn by its caller". Answers `200` with `withdrawn` (how many queued entries were removed), `cancelled` (whether the spawn is withdrawn for good — `false` if it had already begun starting, or there was nothing to cancel) and `queued` (whether it is still waiting); an id that already fired or was never queued is a harmless `200` no-op, never a `404`. The global CLI token may withdraw any spawn; an agent's scoped token only one it originated, never an operator's or a cron's (`403` otherwise). On the mutation budget.

```bash
TOKEN=$(cat ~/.amc/cli-token)

# Unicode-safe — em-dash, arrows, emoji, Chinese, accents all survive intact
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"prompt":"Em-dash — and arrow → and emoji 🚀 and Chinese 你好"}' \
  http://127.0.0.1:19519/project/MyProject/new

# Hotkey workflow that wants the window in front
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"prompt":"continue from where we left off","focus":true}' \
  http://127.0.0.1:19519/project/MyProject/new
```

5. **Hand the token to an external AI safely.** The `omniscio-control` skill bundle reads `~/.amc/cli-token` automatically and POSTs with approval gating enforced server-side — see [omniscio-control.md](omniscio-control.md) (the consolidated entry covering cron, automations, settings, sessions, recipes, projects, tags, away-mode, keybindings, and pending actions).
6. **Trigger pre-approved recipes.** `POST /recipes/run` lets an authenticated agent fire a multi-step Claude workflow without opening Omniscio. Two orthogonal gates apply: the recipe must have `agentTriggerable.enabled` set in the Recipe Editor's "Agent triggering" card (others return 403 — an agent cannot trigger an arbitrary recipe) AND the recipe must have cleared the authoring approval gate (`approvalStatus` ∉ `{pending, rejected}`; pending/rejected recipes return 400 even when the agent-trigger flag is on). A 30-second cooldown per `(recipeId, projectId)` tuple prevents accidental retry-loop double-fires; the engine still enforces its singleton lock so concurrent triggers return 409. `GET /recipes/triggerable` is the discovery endpoint — agents call it first to see what's flagged. Full request/response shapes, error codes, and cost-cap behavior: [agent-trigger-recipes.md](agent-trigger-recipes.md).

## For agents

### Implementation

The HTTP listener binds to `127.0.0.1` (never `0.0.0.0`) in [/src/main/services/cli/cli-server.ts](/src/main/services/cli/cli-server.ts) (the flat `services/cli-server.ts` is now a re-export shim; the impl + per-surface `cli/cli-server-*-routes.ts` files live under `cli/`), which also declares every route, enforces the per-token 10/min cron mutation limit, and hard-codes `requiresApproval: true` on AI-created jobs so clients can't override it. The token is generated in [/src/main/services/config-store/accessors-runtime-state.ts](/src/main/services/config-store/accessors-runtime-state.ts) via `crypto.randomBytes(16).toString('hex')` on first launch, stored encrypted in `config.json`, and mirrored to disk by [/src/main/services/cli/cli-token-file.ts](/src/main/services/cli/cli-token-file.ts). Disk hardening: Unix writes with `mode: 0o600` via `fs.writeFile`; Windows runs `icacls <path> /inheritance:r /grant:r "<user>:F"` (5-second timeout so antivirus scans can't hang startup). The settings UI is `src/renderer/src/features/settings/sections/cli-control/CliControlSettings.tsx` (show/hide + copy + regenerate). Regeneration flows through the `CLI_TOKEN_REGENERATE` IPC handler in [/src/main/ipc/cli-token-handlers.ts](/src/main/ipc/cli-token-handlers.ts), which rewrites the file in place — no restart needed. Session-spawn routing calls through to the same process manager that GUI launches use; cron mutation routing Zod-validates the body and hands off to [/src/main/db/queries-cron-jobs.ts](/src/main/db/queries-cron-jobs.ts) for persistence. User-facing marketing page: [/docs/intro-sandbox/cli-control.html](/docs/intro-sandbox/cli-control.html). **The complete route inventory is machine-derived, not written here.** Every registered route — with the gating tier read out of its own handler, its feature id and its source file — is in `the CLI Control Server route gating catalog` (2,917 routes across the hub and its path-range shards), regenerated by `npm run cli-gating:reindex`. Read that rather than treating this page, or any hand-written list, as the inventory of record: the generated one cannot miss a route or invent one, which a hand-kept list always can.

## Related

- [omniscio-control.md](omniscio-control.md) — the skill bundle that uses `/cron/jobs`, `/automation/*`, `/settings`, `/keybindings/*`, `/recipe/*`, etc. from external AIs
- [create-cron-job-with-ai.md](create-cron-job-with-ai.md) — user-facing walkthrough for the cron surface specifically
- [agent-trigger-recipes.md](agent-trigger-recipes.md) — `POST /recipes/run` + `GET /recipes/triggerable` in detail (agent-fired recipe runs)
- [bug-report-intake.md](bug-report-intake.md) — `PATCH /project/:id/bug-intake` opt-in details and the email triage pipeline it controls
- [inbox-alerts.md](inbox-alerts.md) — `POST /alert` in detail: how an agent drops a persistent row in the user's inbox
- [mobile-remote-access.md](mobile-remote-access.md) — a _different_ server for remote (non-localhost) access from your phone
- `The CLI route gating catalog` — **the** complete, generated route inventory (every route + its gating tier)

This page is split across three parts: [part 2](cli-control-part-2.md) covers the endpoints that change projects, sessions, project docs, away-mode rules and recipes, and [part 3](cli-control-part-3.md) covers the ones that read and drive what you are looking at — coaching data, bookmarks, inbox alerts, screen capture and on-screen highlighting.
