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 withartifactType: "video", anddata.publicUrlopens a share page that plays it in the one video player.shortLink: trueadds a short link,expiresInworks as for a page, and a retry carrying the sameX-Client-Request-Idreturns 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-typewith ahint): another video format (.mov,.webm,.mkv,.avi, …), a file named.mp4whose 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 withPOST /convert({ "inputPath": "<file>", "to": "mp4" }) and publish the MP4 that returns. Without ffprobe installed the MP4 publishes anyway, with awarning. - Page-only fields (
fullBleed,background,commentsEnabled,teamChatChannelId) are ignored for a video, with awarning;customSlugis refused. - A video share is never replaced in place — a video and a page are stored differently, so
updateToken(in either direction) andPOST /share/:token/republishon a video share are refused with400before 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
504if 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 bypath. 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. sourceTypeis not configurable from the body. The CLI route hard-codessourceType: '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
contentover ~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 bypath. - 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/100dvhwithoverflow:hiddenand 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: no100vh/100dvhheight lock onhtml/body, nooverflow:hiddenpage root, no flex-stretch that fills the screen, no bottom-pinned regions (reserveposition:fixedfor genuine overlays). This is independent offullBleed— a full-bleed page still scrolls fine as long as its own document flows. Verify before you hand over the link: open the livedata.urlat 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/notifyOnViewfields — every CLI-published share starts with no password gate, no view cap, view count0, andnotify_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 callPOST /share/:token/protectionwith a{ "password": "<plaintext>" }body — the endpoint derives the hash server-side viaderiveSharePasswordHash()(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:19519and 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 -voutput (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_linkstable)
Last verified 2026-10-04