---
title: Share Artifacts (publish local files and pasted content as links)
---
# Share Artifacts (publish local files and pasted content as links)

## What it is

Share Artifacts turns a local file on your machine, or arbitrary HTML / Markdown / Text / React you paste into a modal, into a **public shareable URL** in one click. The destination is a self-contained HTML page hosted at `https://shares.omniscio.com/s/<token>` — viewers don't need Omniscio, an account, or any extension; they just open the link in any browser and see a styled view of the original content (images render inline, PDFs embed with a viewer, code gets syntax highlighting, markdown renders in an Omniscio-owned document preset with headings / tables / light-dark theming, plain text gets monospace formatting, **HTML and React run live with JS execution just like Claude.ai's artifacts**). Framed artifact share pages (text / code / markdown / paste) include a **Light / Dark toggle** in the header — the viewer's choice persists in `localStorage` across reloads. Full-bleed HTML / React / image shares carry **no** toggle: their content renders inside a sandboxed page (or a viewport-filling image) the toggle can't re-theme — instead those pages take their light/dark from the **artifact itself**. The shell reads the page's own declared `color-scheme` (or its root background) and matches both the backdrop and the **native scrollbar** (and form controls) to it, so a dark share shows a dark scrollbar (and never flashes white) on **every** device — inside the app and on any OS — instead of the browser's default bright bar; an artifact that declares no theme falls back to the OS-following `light dark`. Framed shells still bind their `color-scheme` to the Light / Dark toggle.

This is **distinct from** Artifact Sharing (sessions / messages / selections — see [artifact-sharing.md](artifact-sharing.md)). That feature takes conversation content and turns it into HTML. Share Artifacts takes things that already exist on disk (or in your clipboard) and turns them into HTML. Both feed the same `share_links` table and the same `https://shares.omniscio.com/s/<token>` URL space, but the entry points and supported inputs are different.

Three in-app entry points exist today:

- **File peek toolbar** — open any file in the Peek viewer (click a file path in an agent message, or use the in-app file browser), then click the **Share2 / Publish** icon in the toolbar between Copy and Edit. The current file's absolute path is sent to the publish pipeline; the resulting URL lands in your clipboard.
- **Paste publish modal** — Settings → **Sharing** → **Publish pasted content** button. Opens a modal where you paste arbitrary HTML / Markdown / Text / React, pick a content type, an optional **Title** (the page heading) and **Subtitle** (the field formerly named "Label"), an optional expiration (Never / 1 hour / 1 day / 7 days), and — for HTML / React — an optional **Full-screen** toggle plus a backdrop-colour box, then hit Publish. Omniscio stages the text under `<userData>/published-pastes/<uuid>.<ext>`, runs the same validation pipeline as the file path, uploads to Firebase, and copies the URL to your clipboard. (See **Presentation controls** below.)
- **Chat-link right-click menu** — when Claude produces a file path in a chat message (e.g. `[notes.md](docs/notes.md)`, an attachment chip the user previously dropped into the conversation, or any other markdown link to a file inside the project), **right-click the link** to open a small portal menu. Below the existing **Copy path** item is **Share as public artifact** — pick it and Omniscio publishes the file straight through the same pipeline as File Peek, copying the URL to your clipboard with no confirmation step. File paths resolve through the active session's worktree (or project folder); attachment links (`attachment://<sessionId>/<filename>`) resolve through the main-process attachment store so the renderer never has to know the userData absolute path. Virtual-project links (`__inbox__`, etc.) cannot be resolved and surface a friendly error toast — paste the content through the paste modal instead.

A fourth entry point — the CLI control server's `POST /share/publish` — is documented separately at [share-cli.md](share-cli.md) for callers who want to publish files or content from a script or an external AI agent.

### What gets uploaded

For every successful publish, Omniscio:

1. **SHA-256-hashes the raw bytes of the source file PLUS the presentation knobs** (`title` / `fullBleed` / `background`, plus the free-tier "Made with Omniscio" badge state). This is the dedup key — if the same content _with the same presentation_ has already been published in an active share row, the existing token / URL is returned with `reused: true` and no second upload happens (great for "I keep sharing the same screenshot"). Changing the title, full-screen, or backdrop — or the publisher's plan tier flipping the badge on/off — counts as different content and mints a fresh link instead of returning the old presentation.
2. **Generates a 16-hex-character token** via `crypto.randomBytes(8).toString('hex')`. The token is the URL's path segment (`/s/<token>`); there is no separate authentication layer — the token is the credential. 16 hex chars = 64 bits of entropy, so brute-forcing is not in scope.
3. **Inserts a `pending` row** in the `share_links` table BEFORE the upload starts. If Omniscio crashes mid-upload, the row is reconcilable on next launch (and the orphan is cleaned up automatically when its corresponding blob never materialises).
4. **Uploads** the rendered shell HTML to Firebase Cloud Storage at `gs://agentmc-shares-artifacts/shares/<token>.html`. The shell embeds all content as base64 / inlined CSS / inlined hljs theme, plus the Omniscio app icon as an inlined `data:` favicon (so the browser tab shows Omniscio branding). For **HTML / React** shares, the author's runnable code is uploaded as a **sibling blob** at `shares/<token>.run.html` (served from `/s/<token>/run` under the sandbox CSP — see "Security model"); the shell frames it via `<iframe src>`. Static kinds (markdown / text / code / image / pdf / SVG) have no run doc — the shell is genuinely self-contained on its own.
5. **Flips the row to `published`** with the final URL (`https://shares.omniscio.com/s/<token>`). Telemetry fires (`share_artifact_publish` / `fired`) with `artifactType`, `reused`, `sourceType` ∈ `'file-peek'`, `'paste-modal'`, `'chat-link'`, `'cli'`, `'republish'`, and `autoStaged: boolean` (true when the renderer copied an out-of-scope source path into `<userData>/published-pastes/` before validating) so the in-app Stats view can split traffic by origin and stage path.
6. **Copies the URL to your clipboard** and raises a single success toast — "Published — link copied" or "Already published — reusing link" depending on whether the dedup branch fired.

Pending rows that never reach `published` (because the upload failed or Omniscio crashed) are deleted on the next reconciliation pass.

## Where to find it

### Where to publish from

You can publish:

- **Any file inside an active session's working directory.** The path allow-list is built from every non-archived / non-ended / non-paused session's `resolveSessionWorkDir(session, project)`. Files inside `<project-folder>/.claude/amc-attachments/<sessionId>/` (where chat attachments land) qualify, as does anything else the agent has written into the project tree.
- **Any file inside Omniscio's user-data dir.** `<userData>/published-pastes/` (where the paste modal stages its uploads), `<userData>/attachments/` (the fallback path for chat attachments when the project folder is read-only), `<userData>/bug-reports/`, and so on. The user-data dir is added to the allow-list because Omniscio writes legitimate share material there.
- **Any file you can right-click in chat — even if it's outside both lists above.** When a renderer entry point (file-peek toolbar, chat-link right-click menu, paste modal) hands the service a `sourcePath` that fails the allow-list check, the service **auto-stages** the file by copying its raw bytes into `<userData>/published-pastes/<uuid>/<original name>` (the same dir the paste modal uses, one level down) and re-validating. From the user's perspective there is no second prompt and no error toast — the file just publishes. Telemetry records `autoStaged: true` on the resulting `share_artifact_publish` event so the Stats view can split staged-vs-direct traffic. Symlinks still resolve through `fs.realpath` before the original check, so symlink-escape is not a back door.
- **A credential-shaped file is the one exception, and it is absolute.** A file whose NAME looks like key material — a `.env` and its variants, `.npmrc` / `.netrc` / `.pgpass` and the other credential dotfiles, an SSH private key, a `token` / `secret` / `credential` name, a key-material suffix — is refused outright on every road, including this one. It is never auto-staged: staging renames the file, and the deny-gate can only recognise a file by its name, so a rescued secret would re-validate clean and publish. Expect a plain refusal, with no confirm prompt to click through.
- **The CLI route stays strict.** `POST /share/publish` calls with `sourceType: 'cli'` are rejected with `outside-allowlist` when the path is outside the allow-list — no auto-stage. The trust model splits here: a user gesture (right-click, toolbar button) is treated as consent to copy file bytes; a scripted caller has to explicitly stage its own files into an allow-listed dir first to prevent automated exfil scenarios.

Symlinks are resolved with `fs.realpath` BEFORE the allow-list check, so a symlink pointing from an allowed dir to (say) `C:\Windows\System32\` does not let you exfiltrate system files. Path-traversal sequences (`..\..\..\Users\me\.ssh\id_rsa`) are also resolved before the check — the allow-list compares real paths, not as-typed paths.

## How it behaves

### Supported types

The publish pipeline detects the artifact kind by extension first, then sniffs magic bytes to defend against extension/content mismatch. Recognised kinds:

- **HTML** — `.html`, `.htm`. The author's markup is served as a **separate same-origin document** at `/s/<token>/run` (stored as `shares/<token>.run.html`) and embedded by the share shell via `<iframe src="/s/<token>/run" sandbox="allow-scripts …">` — `src`, not `srcdoc`. The run doc carries a `sandbox` CSP (without `allow-same-origin`), so its `<script>` blocks **execute live** but in an **opaque origin**: the running JS cannot read cookies / localStorage / any sibling share on `shares.omniscio.com`, cannot persist web storage at all, and cannot navigate the top tab. (See "Security model" for why the old `srcdoc` approach ran nothing.)
- **React / JSX** — `.tsx`, `.jsx` (and the "React" choice in the paste-publish modal). Same separate-origin `/s/<token>/run` mechanism as HTML: the run doc loads React 18 + ReactDOM + Babel-standalone from `unpkg.com`, Babel transpiles JSX and TypeScript in the browser, and the bootstrap auto-mounts whichever top-level component you define (`App`, `Component`, or `Demo`) to `#root`. Fallback shows a friendly "No App / Component / Demo defined" message if none is found; runtime errors render in a styled error card instead of crashing silently.
- **Markdown** — `.md`, `.markdown`. Rendered by Omniscio's **one shared markdown renderer** (`renderMarkdown` in [/src/main/services/share/share-markdown.ts](/src/main/services/share/share-markdown.ts) — the same pipeline behind session shares, the Word/PDF export and KMS published pages) inside a dedicated reading shell: centered document column, polished light/dark theme, stronger typography, and no synthetic filename masthead unless you explicitly provide a `title`. The full GFM feature set renders: headings h1–h6, nested and ordered lists, task-list checkboxes (inert), tables with column alignment, blockquotes (multi-line, nested, with lists inside), fenced code with syntax highlighting when a language is named (an unlabeled fence stays plain), inline code, links, images, bold / italic / strikethrough, hard line breaks, horizontal rules and **footnotes** (`[^1]` refs become superscripts; definitions collect in a footnotes section at the end). Raw HTML inside markdown is **sanitized, never executed**: dangerous tags and attributes are removed, `<!-- HTML comments -->` are dropped entirely (they never print as text), and only safe inline markup survives — markdown stays static even though sibling HTML artifacts run scripts. **Do not convert markdown to HTML yourself before publishing** — publish the `.md` path (or `contentType: "markdown"`) and let this renderer do it; a hand-rolled converter is how blockquotes and working-note comments end up printed on the page as text (2026-09-10). A share published before that date keeps its old render until it is republished.
- **SVG** — `.svg`. Inlined into the page (magic-byte check looks for `<svg` within the first 1 KB to allow XML prologues). `<script>`, `<foreignObject>`, `<object>`, and `<embed>` elements inside the SVG are stripped before inlining (see Security below).
- **Images** — `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`. Magic-byte verified (PNG header, JPEG SOI marker, GIF87a/89a, RIFF/WEBP, GIF). Embedded as a single inline base64 `<img>` so the share page is genuinely self-contained — no hot-link to the original.
- **PDF** — `.pdf`. Magic-byte verified (`%PDF-`). Embedded as a base64 `<embed>` so the browser's native PDF viewer renders it inline; the served share page's CSP grants `object-src data:` to permit the plugin data (without it the document area renders blank).
- **Plain text** — `.txt`, `.json`, `.xml`, `.csv`, `.tsv`, `.log`. Wrapped in a `<pre>` block with no syntax highlighting.
- **Code** — `.ts`, `.js`, `.py`, `.css`, `.scss`, `.go`, `.rs`, `.java`, `.rb`, `.sh`, `.ps1`, `.yml`, `.yaml`, `.toml`. Highlighted via `highlight.js` language auto-detection. Note: `.tsx` and `.jsx` route to **React** (above), not Code — they execute by default. If you want syntax-highlighted-only output for a `.tsx` file, paste it through the modal with `Text` selected.

Anything outside that list — Office docs (`.docx`, `.xlsx`, `.pptx`), archives (`.zip`, `.tar.gz`), binaries (`.exe`, `.dll`, `.dmg`), source bundles Omniscio doesn't have a renderer for — is rejected at submit time with `unsupported-type`. If the extension says one thing but the magic bytes say another (e.g. an `.exe` renamed to `.png`), it's rejected with `magic-byte-mismatch`. Empty files are rejected with `empty-file`.

### Presentation controls (title, full-screen, backdrop)

Every publish — in-app or CLI — can set how the share page _presents_, on top of the content itself:

- **Title** — sets the browser-tab title, the link-preview (OpenGraph) title, and any visible share masthead. On the generic framed shell it is the page `<h1>`; on markdown it opts into a document masthead; without it, markdown keeps the filename in metadata only and does **not** inject a visible filename banner. A **full-bleed html / react share (the default)** has no chrome at all, so there `title` sets only the tab + unfurl title — never a visible on-page heading (the page is exactly what the author rendered). The existing **label** becomes the smaller subtitle / kicker where the shell shows one. OG-card-title precedence is **title → label → filename**.
- **Full-screen (full-bleed)** — drops the fixed Omniscio frame (the centred reading column and the heading) so the page fills the whole viewport. **HTML and React shares are full-screen BY DEFAULT** — the author controls the whole page, so a publish that doesn't say otherwise (the AI's `POST /share/publish path=… label=…`, the file-peek Share button, the chat-link right-click) shows the content edge-to-edge with no filename heading and no frame. To get the framed reading view for an HTML/React share instead, pass `fullBleed: false` (or uncheck **Full-screen** in the paste modal). The framed view viewport-locks the page and the artifact fills the space below the heading as a **single scroll region** — never a fixed-height inner pane that double-scrolls against the page. Honored only for **HTML / React / image** shares; every other kind ignores it and stays framed. Markdown's default framed mode is now a deliberate Omniscio document preset rather than the old generic filename wrapper, and image stays framed-by-default (a shared screenshot keeps its caption unless you pass `fullBleed: true`). The HTML/React content still runs inside the same opaque-origin sandboxed iframe pointed at `/s/<token>/run` — full-screen never relaxes the security model. **On a phone it always renders WHOLE + single-scroll, never cut off:** a **responsive** page (one that declares a `@media` query or a `width=device-width` viewport) renders **full-width in its phone layout** — Omniscio hosts it at the device's real width so the author's mobile breakpoints actually fire (without that, mobile browsers lay a framed sub-page out at a ~980px desktop width and shrink it — the "two cramped columns on a phone" bug, fixed 2026-08-10) — and because Omniscio gives it exactly one screen and lets the page scroll itself, **a header the author pinned to the top of the page stays pinned while the reader scrolls** (before 2026-09-06 a page taller than the screen was handed a frame as tall as its whole content, which never scrolls, so every `position: sticky` / `position: fixed` element silently slid away — and whether it happened came down to load timing, so the same page pinned on one device and not another); a **fluid / normal-flow** page (`width:100%`, no breakpoints) is rendered **directly** — exactly the way a browser renders the file, one scrollbar, no shrinking and no measure/scale machinery, so it can never double-scroll; and a page the author built at a **fixed desktop width** (e.g. ~1140px, no responsive CSS) is automatically **scaled down to fit the screen and centred** (the "fit shell" — Omniscio measures the content's natural width and shrinks the whole thing; pinch-to-zoom restores detail), so nothing strands off the side. Either way, a page that locks itself to the viewport (`height:100vh` / `overflow:hidden` — the classic "app shell" that gets cut off) is gently normalized so it flows and scrolls as one document instead of clipping. The author writes no responsive CSS, and needs no share-specific tricks, for any of this to work. **Opened in a background tab, it stays light until you look at it, then reliably appears** — to keep the browser from choking when many heavy shares are opened at once, the fit shell holds the render until the tab is actually viewed, and it reveals through multiple independent signals so it can never get stuck on a blank/black screen waiting for one that never fires.
- **Backdrop colour** — only meaningful with full-screen. Pass a colour (hex or `rgb()` — strictly validated; anything else is ignored) or leave it blank for an **automatic** backdrop: an image gets a soft blurred copy of itself behind it (so a small image never sits in a harsh void — the Spotify / Apple-Music "blurred art" treatment), and an HTML / React page gets a backdrop **matched to the artifact's own theme** — a dark page gets a dark backdrop, a light page a white one (an artifact that declares no theme falls back to an OS-aware neutral) — so a dark share never flashes white before it paints, on **any** device and independent of the viewer's OS light/dark setting. That same artifact-matched theme also drives the page's **native scrollbars** (and form controls), so a dark share no longer shows bright white scrollbars inside the app or on a light-mode machine (the theme is read from the artifact's own `color-scheme` / background — see the share-artifacts contract's scrollbar-theming invariant).

- **Links inside a shared HTML / React page open in a new browser tab.** The artifact runs in a locked-down sandbox whose security policy only permits a short list of destinations, so letting a link navigate that frame does not take the reader anywhere — it replaces the page with the browser's error screen, and there is no way back. Omniscio now routes the click out to a real tab instead, and the shared page stays on screen behind it. In-page jump links (a table of contents) still scroll where they always did, `mailto:` links still open a mail app, and ctrl- or middle-clicking still behaves normally. This travels in the published page itself, so a share published **before 2026-09-11** keeps the old behaviour until you re-publish it.

A change to any of these counts as a **new** share for dedup purposes — see "What gets uploaded" below. In the app, the paste modal exposes the Title field, the Full-screen toggle (shown for HTML / React), and the backdrop box; from a script or an Omniscio-spawned agent, pass `title` / `fullBleed` / `background` on `POST /share/publish` ([share-cli.md](share-cli.md)).

### "Made with Omniscio" badge (free tier)

Shared pages published by **non-paid (free-tier)** users carry a small **"Made with Omniscio"** badge — a fixed pill in the bottom-right corner (a gradient app mark + the wordmark) linking to the product site. It appears on **every** share shell: the framed reading view, the markdown document preset, and the chromeless full-bleed HTML / React / image pages (the surface that previously had no branding at all). **Pro, Team and Enterprise get clean, unbranded pages** — removing the badge is the paid perk. This replaces the old always-on "Shared via Omniscio" footer.

Mechanics:

- **Decided at publish time by one predicate**, `currentUserCanPublishUnbrandedShare()` — the per-tier rule (`SHARE_UNBRANDED_BY_TIER` in the paid-offering manifest: Pro / Team / Enterprise) **OR** an admin-designated per-account `unbranded-shares` add-on. Fail-closed: free, or an unknown / not-yet-resolved tier, shows the badge; only a reliably-known entitlement hides it. It is baked into the uploaded HTML, so an existing share keeps whatever branding it was published with — upgrading affects only **new** publishes. The badge state is folded into the dedup key, so re-publishing the same bytes after a tier change mints a fresh, correctly-branded link instead of returning a stale one.
- **Pure HTML + CSS, no script** — the outer shell's `script-src 'none'` CSP is untouched. The pill is a self-contained translucent-dark chip (its own blurred backdrop + border) so it stays legible on any artifact background or light/dark theme, and it sits on `position: fixed` so it renders identically on all three shells.
- **Soft-trust model** — being baked client-side, it follows the same posture as every "Made with X" badge (a determined free user could strip it from their own local copy). Real paid enforcement (share quotas, pooled-AI allowances) stays server-side; the badge is growth/branding, not a security boundary.
- **No user-facing toggle — but an individual account CAN be approved.** There is deliberately no switch in Settings: a self-serve one would defeat the freemium model. An admin can instead grant the `unbranded-shares` add-on to one account, which unlocks unbranded pages **without moving that user's plan** — the intended path for a customer who needs clean pages on an internal document. It is admin-designated only (seeded from the cloud profile at sign-in, never settable locally), and it is additive: granting it never disturbs another add-on the account already holds.
- **A share that is unexpectedly badged is worth checking against the account’s sign-in.** The badge follows the *effective* tier, and a cached sign-in that can no longer verify itself ages that tier out to free — so a genuinely paid user whose credential broke starts getting badged pages with nothing else to explain it. Omniscio now raises an inbox notice when it reaches that state; see [data-folder-recovery](data-folder-recovery.md).

### Writing a React artifact

The React runtime expects exactly **one top-level component**, named `App` (preferred), `Component`, or `Demo` — the bootstrap auto-mounts whichever it finds first. React 18's `createRoot` API is used internally; hooks (`useState`, `useEffect`, etc.) work out of the box because React 18 is loaded globally before your code runs.

Minimal example:

```jsx
function App() {
  const [count, setCount] = React.useState(0)
  return (
    <div style={{ padding: 16 }}>
      <h1>Counter: {count}</h1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  )
}
```

TypeScript also works — Babel's `typescript` preset strips type annotations:

```tsx
type Props = { name: string }
function App({ name }: Props = { name: 'World' }) {
  return <h1>Hello, {name}!</h1>
}
```

Notes:

- `React` and `ReactDOM` are globals (loaded from unpkg before your code). Use `React.useState`, `React.useEffect`, etc., or destructure: `const { useState } = React`.
- You can import third-party libraries via `<script>` tags **before** your component definition if you load them through the paste modal's HTML option and embed React manually — but the React kind itself doesn't support `import` statements (Babel-standalone runs in non-module mode for in-browser transpilation).
- No SSR, no server APIs, no build tooling — pure client-side. If you need a build step, publish the built output as an HTML artifact instead.
- Render failures (syntax errors, runtime crashes) show in a styled red error card so the viewer sees what went wrong instead of a blank page.
- **Blocked-frame hint (full-bleed HTML/React).** A full-bleed HTML/React share renders the artifact inside a nested sandboxed `<iframe srcdoc>` (the scale-to-fit "fit-host" served at `/s/<token>/run`). A viewer's content-blocker — an ad, script, or privacy extension — can kill that inner `about:srcdoc` frame, leaving the browser's dead "about:srcdoc … temporarily down" page with no explanation. The fit-host detects this: the inner frame's size report doubles as a liveness signal, so if none arrives within ~5s (the frame's scripts never ran) it reveals a dismissible, theme-aware hint banner ("a browser extension may be blocking this — open in a private/incognito window or disable your blocker for this page"), and hides it again if a late report arrives (a merely-slow share flashes at worst). The share itself is not broken — it serves 200 and renders in a clean browser; the hint just explains a viewer-side block. Full-bleed only (that's the path with the nested frame). See share-artifacts-contract `full-bleed-fits-a-phone`.

### Per-share protection (password / view cap / notify-on-view)

Every published share also carries four protection fields. The defaults are off (no password, no cap, no notification, count tracked but invisible) and the controls live on the **Edit** modal in the Shares sidebar view — see [shares-view.md § Editing a share](shares-view.md) for the UX details. Summary of the on-the-wire model:

- **Password**: optional. The plaintext is hashed in the renderer via SubtleCrypto (PBKDF2-SHA256, 600,000 iterations, 16-byte random salt, 32-byte derived key) and only the hex-encoded `{ passwordHash, passwordSalt }` ship across IPC via the dedicated `SHARE_SET_PROTECTION` channel. The pair must move together — partial sets are rejected. The Cloud Function holds the hash + salt in the Firestore mirror, presents a password prompt to viewers, re-runs the same PBKDF2 derivation on submit, and constant-time-compares. `passwordHash` / `passwordSalt` are stripped from every share-row IPC response — only `hasPassword: boolean` reaches the renderer.
- **View cap**: optional positive integer (1 – 1,000,000). When `view_count >= view_cap` the Cloud Function returns a 410 "view cap reached" page without writing a view event. Useful for "one-time view" or "limited-distribution" shares.
- **View count + last viewed**: always tracked. The Cloud Function writes a `share_view_events` row on every successful render and atomically increments `share.view_count` in Firestore. The desktop poller (60s, jittered, 30s initial delay) mirrors the deltas back into the local SQLite `shares` row + appends to the local `share_view_events` audit log.
- **Notify on view**: opt-in per share. When enabled, the poller fires a desktop notification ("Share viewed: <label>") for each share whose count advanced since the last poll. The row update commits before the notification fires — crash-safe ordering means at most one missed notification on a crash, never a double-fire.

Protection edits propagate through two distinct channels: `SHARE_UPDATE` carries `label / expiresAt / liveUpdating / viewCap / notifyOnView / commentsEnabled`; `SHARE_SET_PROTECTION` carries `passwordHash + passwordSalt` (both, or both null). Splitting the channels keeps the plaintext-password handshake off the general patch surface.

### Commenting

Shares can optionally allow public comments. The `commentsEnabled` field on a share toggles a comment widget in the published page — the comment app bundle is injected into the share shell when enabled. During publish, the paste modal and CLI flow expose a **Team Chat channel picker** so the share can be linked to a Team Chat channel; new comments on the share are then cross-posted as thread replies to that channel via the `onCommentCreate` Cloud Function. Full contract: [share-comments-contract.md](../../.claude/memory/contracts/share-comments-contract.md).

Privacy + audit-log details: [share-view-events.md](share-view-events.md).

### Republish as new

Useful when you want a fresh URL for the same content (different recipient, separate tracking, etc.). The **Republish as new** action in the Shares view row menu mints a brand-new token, points it at the same uploaded blob (re-uploaded under the new token, dedup deliberately bypassed), and revokes the old token. Available only while the original is still active and has a backing `sourcePath` (so re-uploading is possible). Full UX + IPC contract: [shares-view.md § Per-row actions](shares-view.md) and the `SHARE_REPUBLISH` channel.

## Related

- [Share Artifacts (publish local files and pasted content as links) (part 2)](share-artifacts-part-2.md) — the continuation of this page.
- [Share Artifacts (publish local files and pasted content as links) (part 3)](share-artifacts-part-3.md) — the continuation of this page.

- [shares-view.md](shares-view.md) — the top-level sidebar view that lists / edits / revokes / deletes every share you've created (replaces the old Settings → Sharing list)
- [share-cli.md](share-cli.md) — same publish pipeline, called from a script or external AI via the CLI control server
- [share-view-events.md](share-view-events.md) — privacy model + audit-log shape for the view-tracking pipeline
- [artifact-sharing.md](artifact-sharing.md) — the sibling feature for sessions / messages / selections (different inputs, same `share_links` table and URL space)
- [cli-control.md](cli-control.md) — the CLI control server that hosts `POST /share/publish` and the three lifecycle endpoints
