Omniscio documentation
Browse all documentation
  1. Getting Started13
  2. Sessions & Agents115
  3. Inbox & Notifications59
  4. Projects & Tasks95
  5. Automation & Scheduling75
  6. Knowledge & Memory26
  7. AI Features60
  8. Integrations100
  9. Plugins & Marketplace33
  10. Cloud & Teams56
  11. Settings & Customization58
  12. Account & Billing28
  13. Troubleshooting84
  14. CLI & API Reference22
  15. Legal & Policies4
  16. Uncategorised22

Share via CLI (publish, list, revoke, delete from a script or AI)

Four endpoints on the local CLI control server at http://127.0.0.1:19519 let any tool that can make an HTTP request publish files or pasted content as shareable links, list existing shares, revoke (soft-disable) a share, or delete (hard-remove) a share row.

What it is

Four endpoints on the local CLI control server at http://127.0.0.1:19519 let any tool that can make an HTTP request publish files or pasted content as shareable links, list existing shares, revoke (soft-disable) a share, or delete (hard-remove) a share row. The endpoints mirror the renderer-side IPC handlers exactly — same publish pipeline, same path allow-list, same content-hash dedup, same Firebase upload, same share_links table — so a CLI publish is indistinguishable from an in-app publish at the data layer. The only telemetry difference is sourceType: 'cli' on the share_artifact_publish event so the Stats view can split CLI traffic from in-app traffic.

For the user-facing flow (paste modal, file peek toolbar, supported types, limits, security model), see share-artifacts.md. This page is the CLI-caller reference: route shapes, body schemas, status codes, and curl recipes.

Where to find it

There is no screen for this — it is the reference for the local control server an outside script or AI calls. The token such a caller needs is created and regenerated in Settings → CLI Control.

How it behaves

Authentication

All four endpoints require the Omniscio bearer token in the Authorization header. The token is auto-generated at first launch and stored at ~/.amc/cli-token (%USERPROFILE%\.amc\cli-token on Windows) with OS-level file-permission hardening (mode 0600 on Unix, ACL-restricted on Windows). Settings → CLI Control shows the token and offers Regenerate.

Never echo the token to chat, logs, or terminal output. Fetch it via the DPAPI vault helper in agent code (so the value never appears in argv or environment dumps):

# Unix / macOS / Git Bash on Windows — agents with repo access
TOKEN=$(cat ~/.amc/cli-token)

# PowerShell DPAPI vault — preferred when running as your user
$TOKEN = & "$HOME/.claude/secrets/get-secret.ps1" amc-cli

If a token leaks (curl -v in chat, accidental log line, screenshot), regenerate it immediately at Settings → CLI Control.

Endpoints

POST /share/publish — publish a file or pasted content

Auth: bearer token in Authorization header (query-string ?token= is rejected, like every other mutation route). Body cap 1 MB at the dispatcher level.

Rate limit — publishing has its OWN per-session budget (12/minute), NOT the shared mutation pool. It used to sit on the global 10-mutations-per-minute bucket shared by every session and every mutating route, so another session's unrelated writes could refuse your publish after only two or three artifacts. It now has a dedicated per-session budget — your own publishes are the only thing that can exhaust it. POST /report-template/render shares that same budget (it performs the same publish). Revoke and delete still use the shared mutation pool.

A refused publish returns 429 with an honest Retry-After (seconds until a slot actually frees, not a flat 60) plus a retryAfterSeconds body field — read one of those instead of guessing a sleep interval. Some clients hide response headers: PowerShell 5.1's Invoke-RestMethod raises a terminating error on a 429 and discards them, so read the body field (or catch the exception and inspect $_.Exception.Response).

The budget is keyed on your session's PROVEN identity, so rotating the X-AMC-Source-Session-Id header does not grant a fresh one.

Encoding — prefer path over content. For a local file, send its path: Omniscio reads the bytes server-side as UTF-8, so a wrong-encoding read on the caller's side (e.g. PowerShell Get-Content -Raw, which defaults to Windows-1252 — not UTF-8) cannot garble multibyte characters (em-dashes, bullets, curly quotes, emoji). If you must send content, read and transmit it as UTF-8 (PowerShell: [IO.File]::ReadAllText($p,[Text.Encoding]::UTF8) plus a UTF-8 byte body). On suspected double-encoding the response adds a non-fatal warning (see below) — it never rewrites your content. Always verify the LIVE url, not the local file.

Body schema (JSON, exactly one of path / content required):

field type required notes
path string (1–2048 chars) XOR with content Absolute path to a file inside an active session's workdir, ~/Claude, the calling session's own scratch folder ($AMC_SESSION_TMP — send X-AMC-Source-Session-Id to claim it; it is that session's folder only, never another session's), a folder the user granted the app, or Omniscio's published-pastes dir. Symlinks are resolved before allow-listing. A refusal lists the exact roots it accepted in allowedRoots An H.264 MP4 (.mp4 / .m4v) publishes as a video — see Videos and live films
content string (1–2 MB) XOR with path Inline UTF-8 content to publish — staged under <userData>/published-pastes/<uuid>.<ext> before going through the pipeline
contentType 'html' / 'markdown' / 'text' / 'react' optional Defaults to text. Drives the staging-file extension and the share-page renderer. 'markdown' (or a path ending in .md) renders through Omniscio's shared markdown renderer — full GFM incl. tables, task lists, footnotes and highlighted code, HTML comments dropped, raw HTML sanitized — so never pre-convert markdown to HTML yourself. 'react' stages a .tsx and runs through the React/JSX runtime (same as the paste modal's React choice) — so you can publish a live React component from inline content, not only from a .tsx file by path
label string (≤ 200 chars) optional Display label in the Settings → Sharing list. Defaults to the source filename when omitted
expiresIn int seconds (60 to 60 × 60 × 24 × 365) optional Defaults to 30 days (2592000) when omitted. Pass 3600 for 1h, 86400 for 1d, 604800 for 7d, 2592000 for 30d
title string (≤ 200 chars) optional Browser-tab title + link-preview (OG/unfurl) title. Also the visible page <h1> on a framed share (markdown / text / code / image) — but a full-bleed html / react share (the default) has no chrome, so there title sets only the tab + unfurl card, never a visible on-page heading. label stays the smaller subtitle. Defaults to the source filename
fullBleed boolean optional Chromeless, viewport-filling render — no 960px frame, no heading, no footer. Honored for html / react / image only; ignored (framed) for every other kind. Defaults ON for html / react (a self-contained page owns the whole surface), OFF for everything else — pass fullBleed:false to force the framed reading view on an html/react share
background string (≤ 64 chars) optional Full-bleed backdrop colour — hex (#rgb/#rrggbb/#rrggbbaa) or rgb()/rgba() only; anything else is dropped for an automatic backdrop (a blurred copy of the image for images, a theme-aware neutral for html/react). Read only when fullBleed is honored for the kind
raw boolean optional Hand back the FILE rather than a page about it: the link returns the artifact’s own bytes, typed from its extension and named after it, so a recipient (or curl) gets the file. Omitted → decided by the file — a data file (.json .csv .tsv .txt .xml .log) hands itself back and everything else keeps its page; pass raw:false to force a page, or raw:true to force the file. Only meaningful with path. An extension the shared table cannot serve is refused as unsupported-type
commentsEnabled boolean optional Override the global comments default for this share. true enables the comments sidebar, false disables it regardless of the user's shareCommentsEnabled / shareCommentsDefaultOn settings. When omitted the share inherits the global default
teamChatChannelId string (≤ 100 chars) optional Link this share to a Team Chat channel so the Cloud Function cross-posts new comments as thread replies. The channel must already exist. Persisted on the DB row + mirrored to Firestore; only applied to new publishes (ignored on dedup hits)
updateToken string (1–256 chars) optional In-place update — refresh the artifact share with THIS token (same public URL, new content) instead of minting a fresh link. The target must be a live (non-revoked, non-expired) artifact share; a thread / digest / unknown token is a 400 No such active share to update. Its existing expiry is preserved (expiresIn is ignored on an update). The response returns the SAME url with updatedInPlace: true; on an old-relay fallback it mints a new link + retires the old (updatedInPlace: false)

Success response (200):

{
  "ok": true,
  "data": {
    "token": "a1b2c3d4e5f6a7b8",
    "url": "https://shares.omniscio.com/s/a1b2c3d4e5f6a7b8",
    "publicUrl": "https://shares.omniscio.com/s/a1b2c3d4e5f6a7b8",
    "artifactType": "image",
    "reused": false,
    "contentHash": "9f2b…",
    "sizeBytes": 22698
  }
}

reused: true means the SHA-256 content hash matched an existing active share; no second upload happened and the existing token / url were returned. artifactType is one of 'html', 'markdown', 'svg', 'image', 'pdf', 'text', 'code'. updatedInPlace appears only when you passed updateToken: true = the SAME link now serves your new content; false = the relay could not reuse the token, so a fresh link was minted and the old one retired (read url for the new link).

contentHash + sizeBytes describe the blob this call actually STORED, and match what GET /share/list reports for the token. They are the way to verify an in-place update landed: ok: true tells you the call succeeded, not that the bytes you sent are the ones now being served — so on an updateToken publish, compare sizeBytes against your source and treat a mismatch as a failed refresh. Both are absent on a reused: true dedup hit, where nothing new was written.

When the user's Open shares in app setting (sharesOpenInApp) is ON, data.url is swapped to a deep-link URL (omniscio://share/<token>) that opens the share in Omniscio's built-in viewer instead of the browser. data.publicUrl always contains the HTTP URL regardless of the setting. Callers that always want the browser URL should read publicUrl; callers that want the user's preferred link should read url.

When the user has also enabled Show new shares automatically (sharesAutoOpenOnCreate, opt-in default OFF, sub-toggle of sharesOpenInApp), a successful publish additionally raises a one-click "Go there" toast on the user's screen via the agent-foreground nav-suggestion path (suggestAgentNavigation()). The toast is never a forced view-switch — if the user is in another app it becomes a durable inbox card instead. The toast carries only the share token so an arbitrary URL can never be coerced into the viewer. Toasts coalesce per source-session (a looping agent bumps one toast, latest label wins). This side-effect is transparent to callers: the publish response shape is unchanged.

A non-fatal warning string may appear alongside data when you publish via content and the text looks double-encoded (mojibake) — e.g. an em-dash arriving as the three chars â€" because the file was read in the wrong code page before upload. The share still publishes; the warning is advisory. The fix is to re-publish by sending the file path (read as UTF-8) instead of content. The server never auto-repairs the content (that would corrupt text that legitimately contains those characters). Genuinely-invalid UTF-8 is still rejected with a 400 earlier in the pipeline. Only content is scanned — a path publish never carries this warning.

The same warning also carries what a render of your published page found. After the artifact is live, Omniscio loads it once offscreen at phone size and reports what it saw. It is advisory only — a finding never refuses, delays, or changes a publish, and a check that cannot run stays silent rather than claiming the page is fine. Three things get reported:

Finding What it means What to do
N of M blocks did not reach the rendered page A branded report's own prose is missing from the page a reader loads — usually a block written in a shape no style dispatches on. Two published reports lost 100% of their body this way and still returned success. Write blocks as {"p": "…"}; fix the shapes and re-publish with updateToken.
this artifact's own script threw while rendering Your page's JavaScript raised an uncaught error, so the reader sees only whatever had been drawn before it stopped. Fix the script and re-publish with updateToken.
the page is Npx wider than a 390px phone Content sits off the right edge, inside a frame a reader cannot scroll sideways. Let the layout wrap instead of fixing a width.

There is deliberately no "this page looks empty" warning. Judging emptiness by counting visible text was measured against every published artifact and was wrong every time it fired — a video, an animation and a design mockup all legitimately carry a few words — while missing the report that had actually lost its body.

Error responses:

status error string cause
400 outside-allowlist Path doesn't resolve into an active workdir, ~/Claude, this session's scratch folder, a granted folder, or published-pastes — the body's allowedRoots is the exact list that was accepted
400 unsupported-type File extension not in the supported list (Office, archives, binaries); or a video that is not an H.264 MP4 (the body then carries a hint to convert it through POST /convert)
400 magic-byte-mismatch Extension says one thing, the file's first bytes say another
400 empty-file Zero-byte source
400 No such active share to update updateToken doesn't resolve to a live artifact share (revoked / expired / digest / thread / unknown)
400 A video share cannot be updated in place… updateToken aimed at a video share, updateToken with a video path, or a republish of a video share — publish the new video as a new share
400 A custom link name is not available for a video share. customSlug sent with a video path
400 (zod parse error) Body validation failed — schema fields out of range, both path and content set, etc.
401 unauthorized Missing or invalid bearer token
413 size-cap Source exceeds the type's raw-bytes cap (22 MB image, 18 MB PDF) or rendered HTML exceeds 25 MB
429 Rate limit exceeded (12 artifact publishes/minute for one session) YOUR session published 12 artifacts in the past minute. Carries Retry-After + a retryAfterSeconds body field — wait that long rather than guessing. Another session's traffic can no longer cause this.
502 firebase-upload-failed Generic upload error — check Settings → Sharing → Firebase config and Omniscio's main.log
507 firebase-quota Spark-plan free-tier exceeded — prune old shares or upgrade to Blaze
504 Publishing is taking too long — please try again The publish ran out of its wall-clock budget — 45 s for a page; for a video, a budget sized to the file, up to 15 minutes. Nothing was published. A page's object uploads each carry their own bounded attempt that fits inside that 45 s, so one slow upload can no longer eat the whole budget and a retry still runs; a page within the 25 MB cap completes in seconds. A 504 therefore means the storage host answered slowly repeatedly, not that the page was big. A video's budget scales with its size on purpose — a 26 MB file is allowed ~3.5 minutes because that is what it needs on an ordinary uplink — so on a video a 504 means the link stayed slower than 128 KiB/s for the whole attempt

Recipes:

Publish a file by path:

TOKEN=$(cat ~/.amc/cli-token)
curl -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"path":"C:\\Software Projects\\my-project\\screenshot.png","label":"Bug repro"}' \
  http://127.0.0.1:19519/share/publish

Publish inline Markdown with 24-hour expiration:

TOKEN=$(cat ~/.amc/cli-token)
curl -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "content":"# Title\n\nSome **markdown**.",
    "contentType":"markdown",
    "label":"Notes",
    "expiresIn":86400
  }' \
  http://127.0.0.1:19519/share/publish

Publish inline HTML that never expires:

TOKEN=$(cat ~/.amc/cli-token)
curl -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"content":"<h1>Hello</h1>","contentType":"html","label":"Greeting"}' \
  http://127.0.0.1:19519/share/publish

Publish a full-screen HTML dashboard with a custom title (no Omniscio frame). Note that html / react are full-screen by default now, so the "fullBleed":true below is explicit-but-redundant — kept here to show the field; pass "fullBleed":false instead when you want the framed reading view:

TOKEN=$(cat ~/.amc/cli-token)
curl -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "content":"<div style=\"display:flex;height:100vh;align-items:center;justify-content:center\">Hi</div>",
    "contentType":"html",
    "title":"Live Dashboard",
    "fullBleed":true,
    "background":"#0b0e14"
  }' \
  http://127.0.0.1:19519/share/publish

Publish with comments enabled and linked to a Team Chat channel:

TOKEN=$(cat ~/.amc/cli-token)
curl -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "path":"/path/to/report.md",
    "label":"Weekly report",
    "commentsEnabled":true,
    "teamChatChannelId":"channel-abc123"
  }' \
  http://127.0.0.1:19519/share/publish

Update an existing share's content in place (same URL) — regenerate the file, then point the share's own token at the fresh bytes:

TOKEN=$(cat ~/.amc/cli-token)
# ...regenerate C:\...\report.html with new content at the SAME path...
curl -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"path":"C:\\...\\report.html","contentType":"html","updateToken":"a1b2c3d4e5f6a7b8"}' \
  http://127.0.0.1:19519/share/publish
# → { "ok": true, "data": { "url": "https://.../s/a1b2c3d4e5f6a7b8", "updatedInPlace": true, ... } }

Videos and live films

  • A video publishes by path — pass an H.264 MP4 (.mp4 / .m4v). It skips the page builder and goes to the one video publisher (the same one screen recordings use), after the same folder allow-list check as any file. The reply has the usual shape with artifactType: "video", and data.publicUrl opens a share page that plays it in the one video player. shortLink: true adds a short link, expiresIn works as for a page, and a retry carrying the same X-Client-Request-Id returns the first share. The page it opens on is labelled "Shared video", and its Download control is the download icon in the player's bottom bar. Each publish makes a new share: a video is never de-duplicated by content, so publishing the same file twice makes two links.
  • Refused before anything uploads (400 unsupported-type with a hint): another video format (.mov, .webm, .mkv, .avi, …), a file named .mp4 whose first bytes are not an MP4, or an MP4 whose video ffprobe reports as something other than H.264 (the one format every browser plays). Convert it with POST /convert ({ "inputPath": "<file>", "to": "mp4" }) and publish the MP4 that returns. Without ffprobe installed the MP4 publishes anyway, with a warning.
  • Page-only fields (fullBleed, background, commentsEnabled, teamChatChannelId) are ignored for a video, with a warning; customSlug is refused.
  • A video share is never replaced in place — a video and a page are stored differently, so updateToken (in either direction) and POST /share/:token/republish on a video share are refused with 400 before anything uploads. Publish the new video as a new share.
  • A large video takes a while — the route allows about 4 minutes for the upload (a page stops at about 45 s) and answers 504 if it runs out, with nothing published.
  • A page's uploads are budgeted against that 45 s, not against a fixed upload timeout. Each page object — the thumbnail, the runtime document and the shell — is given a bounded attempt that finishes well inside the route's budget, and the runtime document and the shell upload at the same time rather than one after the other. That pairing is why a page's publish time no longer scales with how many objects it stores, and why one slow upload can no longer turn a small page into a 504. The public link is gated on the share's mirror record, which is written only after both objects have landed, so neither is ever served before the other exists.
  • A film drawn live in code (canvas frames, an optional soundtrack) plays through the same player: fetch the player's script from GET /video-player/script (raw JavaScript, any session token), put it in a <script> tag in your HTML page, mount the film, and publish the page by path. Never build your own video controls.

Publish an MP4 with a short link:

curl -X POST \
  -H "Authorization: Bearer $AMC_CLI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"path":"C:\\Users\\me\\Claude\\launch-film.mp4","label":"Launch film","shortLink":true}' \
  http://127.0.0.1:19519/share/publish

A film drawn in code — the page carries the player script, then mounts the film:

<div id="film"><canvas id="frame" width="1280" height="720"></canvas></div>
<script>/* the text GET /video-player/script returned */</script>
<script>
  const canvas = document.getElementById('frame')
  const ctx = canvas.getContext('2d')
  const media = OmniscioVideoPlayer.createTimelineMedia({
    duration: 20, // seconds
    render: (t) => { /* draw the frame for time t (seconds) onto ctx */ }
    // audio: an <audio> element or URL — when given, it keeps time and pitch holds at any speed
  })
  OmniscioVideoPlayer.mount(document.getElementById('film'), { media, surface: canvas })
</script>

GET /share/list — list every share row

Auth: bearer token in Authorization header. Not rate-limited (read-only) but still bearer-gated because share rows expose public content URLs.

Query string:

param values notes
includeRevoked true / unset When true, revoked rows are included in the result

Success response (200):

{
  "ok": true,
  "data": {
    "shares": [
      {
        "id": 42,
        "token": "a1b2c3d4e5f6a7b8",
        "url": "https://shares.omniscio.com/s/a1b2c3d4e5f6a7b8",
        "label": "Bug repro",
        "scope": "conversation",
        "artifactKind": "image",
        "status": "published",
        "revoked": false,
        "createdAt": "2026-05-14T12:34:56.000Z",
        "expiresAt": null,
        "...": "..."
      }
    ]
  }
}

Field set matches share_links schema — see src/main/db/queries-share.ts for the exact column list. The same row shape is returned by the renderer-side SHARE_LIST IPC handler.

Recipe:

TOKEN=$(cat ~/.amc/cli-token)
curl -H "Authorization: Bearer $TOKEN" \
  "http://127.0.0.1:19519/share/list?includeRevoked=true"

POST /share/:token/revoke — soft-disable a share

Auth: bearer token, 10 mutations/minute rate limit.

Marks the row revoked = 1 (keeps the DB row + the blob in Firebase storage but invalidates the URL — viewers see a 410-style "this share has been revoked" page). Use this for shares you might want to audit later or un-revoke (DB-only operation, no Firebase calls).

Success response (200):

{ "ok": true }

Error responses: 401 (no token), 404 (Share not found — the path token doesn't match any row), 429 (rate limit).

Recipe:

TOKEN=$(cat ~/.amc/cli-token)
curl -X POST -H "Authorization: Bearer $TOKEN" \
  http://127.0.0.1:19519/share/a1b2c3d4e5f6a7b8/revoke

DELETE /share/:token — hard-delete a share row

Auth: bearer token, 10 mutations/minute rate limit.

Deletes the DB row AND removes the Firebase blob. The URL returns 410 Gone afterwards. Irreversible.

Success response (200):

{ "ok": true }

Error responses: 401 (no token), 404 (Share not found), 429 (rate limit).

Recipe:

TOKEN=$(cat ~/.amc/cli-token)
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
  http://127.0.0.1:19519/share/a1b2c3d4e5f6a7b8

Behavioural notes

  • Dedup is global AND presentation-aware — findActiveByContentHash() matches against every active (non-revoked, non-expired) row regardless of who published it (in-app, CLI, IPC), so a CLI publish can reuse a token first generated by the paste modal and vice-versa. The dedup key folds the raw bytes plus the presentation knobs (title / fullBleed / background) together, so re-publishing identical bytes with a different title / layout / colour mints a fresh link instead of returning the old presentation; identical bytes and identical presentation reuse the same token.
  • sourceType is not configurable from the body. The CLI route hard-codes sourceType: 'cli' server-side so the telemetry origin cannot be spoofed.
  • No inbox approval gate — publish is apply-immediately. The design rationale is that publish doesn't spawn Claude (no real-money cost from approval bypass) and the renderer-side flow is also apply-immediately, so gating only the CLI would be asymmetric. Firebase storage costs are real but bounded by the shared 10-mutations/min rate limit.
  • Retries: send X-Client-Request-Id. A retry carrying the same id as a finished publish returns that share instead of minting another — for a page and a video alike. For a page, content-hash dedup also returns the same token for the same bytes; a video is never de-duplicated by content, so the same file published twice makes two shares.
  • The 1 MB body cap is dispatcher-level, so any content over ~1 MB will be rejected at the parse layer even though the Zod schema allows up to 2 MB. For >1 MB inline content, stage the bytes to a file first and publish by path.
  • Build a scrollable document, not a full-viewport app shell (the #1 mobile-render failure). A share renders inside a sandboxed iframe whose height is not the phone's visible viewport (mobile browser chrome + the sandbox wrapper eat vertical space), so an artifact locked to 100vh/100dvh with overflow:hidden and a bottom-pinned bar (a composer, toolbar, or footer) clips that bar off-screen and unreachable — the user cannot scroll or zoom to it. Author every published artifact as natural top-to-bottom flow: no 100vh/100dvh height lock on html/body, no overflow:hidden page root, no flex-stretch that fills the screen, no bottom-pinned regions (reserve position:fixed for genuine overlays). This is independent of fullBleed — a full-bleed page still scrolls fine as long as its own document flows. Verify before you hand over the link: open the live data.url at a short mobile height (~390×600) and confirm the key content (especially any input box or button) is visible without scrolling past the fold.
  • CLI publishes are unprotected by default. The body schema does NOT accept passwordHash / viewCap / notifyOnView fields — every CLI-published share starts with no password gate, no view cap, view count 0, and notify_on_view = false. To add a password after publishing, either open the resulting share in the in-app Shares view and use the Edit modal (UX detailed in shares-view.md § Editing a share), or call POST /share/:token/protection with a { "password": "<plaintext>" } body — the endpoint derives the hash server-side via deriveSharePasswordHash() (so no crypto has to be duplicated in shell callers) and re-mirrors the credential immediately; pass { "password": null } to clear it. The in-app Edit modal still hashes in the renderer via SubtleCrypto; the CLI route instead accepts the plaintext over the loopback, bearer-gated connection and never persists it. This route sets or clears the password only — view cap and view-notify stay in-app.

Security model

Same as the in-app flow — see share-artifacts.md § Security model. The CLI surface adds two specifically-CLI considerations:

  • Localhost-only: the CLI control server binds to 127.0.0.1:19519 and is not reachable from other machines. A misconfigured router cannot expose it.
  • Bearer token treatment: the token is the credential for every mutation. Never include it in curl -v output (which dumps headers to terminal), never paste it into chat, never commit it to a repo. Rotate at Settings → CLI Control → Regenerate if any of those happen.

For agents

Auto-publish from Omniscio-spawned agents

These endpoints aren't only for external scripts — they're how every agent Omniscio spawns surfaces a visual result in your Shares panel, with no per-machine setup. Omniscio folds a publish instruction into the system prompt of every session it starts: when an agent produces a self-contained HTML artifact you should look at (report, chart, dashboard, slide deck, diagram), it POSTs it to /share/publish so it lands in Omniscio → Shares and opens on any device.

The agent authenticates with the CLI control token, which Omniscio hands to the spawn directly as the AMC_CLI_TOKEN environment variable (falling back to ~/.amc/cli-token). Because the token arrives in the env, the publish works even if the on-disk token file is missing. The agent writes the artifact to a file in its working directory (already on the publish allow-list) and passes its path: Omniscio reads that file as UTF-8 itself, so a wrong-encoding read on the agent's side cannot garble the output (dashes, bullets, curly quotes, emoji). Inline content is only a fallback for when no file can be written, and the server flags a non-fatal mojibake warning if that content looks double-encoded. The agent gives you the resulting data.url and is told to prefer this over emailing the file or any external upload script.

The same instruction tells an agent that a video publishes by its H.264 .mp4 path, and that a film it draws in code plays through the one video player via GET /video-player/script — never through controls of its own.

The upshot: anything an agent shares shows up in one place — the Shares view — where you can preview, rename, set an expiry/password, or revoke it. The wiring (the SHARE_PUBLISH_SHIM system-prompt fragment + the AMC_CLI_TOKEN spawn-env hand-off) is specified in backend-spawn-contract.md § 8.

Related

  • share-artifacts.md — the in-app flow with the same publish pipeline (paste modal + file-peek toolbar)
  • cli-control.md — the CLI control server's design, auth model, and full route catalogue
  • artifact-sharing.md — the sibling feature for sessions / messages / selections (different inputs, same share_links table)

Last verified 2026-10-04