Share AI coding-harness chat sessions (ZCode, Claude Code, Codex, Oh My Pi) over the web as read-only, password-protectable, expiring links — with secrets redacted on the server before anything is stored or sent.
- Opt-in publishing. Nothing is shared until the owner runs
quire publish. - Password protection (optional per share) with argon2id hashing and a
stateless, per-share unlock cookie (
HttpOnly,Secure,SameSite=Strict, 30-minute TTL). - Expiration. An expired share returns
410and can never be read again. - Server-side redaction. Ordered rules cover private keys, JWTs, provider API keys, authorization credentials, connection strings, secret assignments and CLI arguments, bare tokens, private IPs, local paths, and encoded or embedded secret-bearing content. Redaction is authoritative at ingestion: only redacted content is persisted, and the viewer's browser never receives a secret.
- Long-session friendly viewer. Vue 3 + Tailwind CSS v4, markdown + Shiki
syntax highlighting, infinite-scroll lazy loading (50 messages per page),
and automatic dark mode via
prefers-color-scheme. - No existence oracle. Unknown and revoked tokens return byte-identical
404s. - Rate limiting. 5 failed unlocks → 15-minute lockout per token+IP; 120 requests/minute per IP across the public API.
pnpm workspace monorepo:
| Package | What it is |
|---|---|
server/ |
Hono + Drizzle (Postgres) API, redaction engine, security layer |
cli/ |
quire publisher CLI with adapters for ZCode, Claude Code, Codex, and OMP |
plugin/ |
/share for Claude Code/ZCode plus a native Codex $share skill |
web/ |
Vue 3 + Vite + Tailwind v4 read-only viewer |
e2e/ |
Playwright full-stack tests (drives the Docker stack) |
Data flow: harness session → adapter and CLI shape it → owner confirms (or
the agent passes --yes) → the CLI generates an in-memory content key and
uploads the raw shaped chunks over authenticated HTTPS → the server performs
authoritative redaction and seals each chunk as an AES-256-GCM ciphertext
envelope → the viewer's browser fetches the envelopes and decrypts them
locally. The content key never reaches the server: it rides in the
authenticated upload HTTP, then in the share URL's fragment (#<key>), which
the CLI appends locally.
Shares live under /chats/<shareId>#<key> (viewer) and
/api/v2/public/shares/:shareId/* (API). Share ids are 128-bit
crypto-random; session ids never appear in URLs.
- Single
QUIRE_API_KEY(Bearer, constant-time compare) guards owner routes. - Unlock cookies are HMAC-SHA256 over
<token>|<expiry>, signed withUNLOCK_SECRET— no server-side session state. - Strict CSP,
X-Frame-Options: DENY,Referrer-Policy: no-referrer,nosniff, HSTS. Uniform error bodies{error:{code,message}}. - 20 MB request cap (
413). No secrets in logs. - See
docs/superpowers/specs/2026-08-23-zcode-session-sharing-design.mdfor the full design and threat reasoning.
Sealed shares encrypt the redacted content with AES-256-GCM in the browser:
the content key appears only in the authenticated upload HTTP and then in the
share URL's fragment (#<key>), never in the server's storage, logs, or
public HTTP. The server stores ciphertext envelopes in plain Postgres and
serves them; only the browser holding the key can decrypt. The fragment is
required — a link without it shows a missing-key error page and makes no
network call. GET /metrics exposes the safe pipeline counters
(counts/bytes/latency/status only).
Operational runbook — migration, metrics, rollback, backup/restore:
docs/operations/sealed-shares.md.
pnpm install
pnpm db:test:up # Postgres on :54329 for tests
pnpm typecheck
pnpm test # unit tests for all packages + E2E (needs Docker)Run the server against a local Postgres:
cp .env.example .env # fill in QUIRE_API_KEY / UNLOCK_SECRET
pnpm --filter @quire/web build
pnpm --filter @quire/server build
cd server
node --env-file=../.env dist/index.jsRun the built server from server/ so its ./drizzle migrations directory
resolves correctly. DATABASE_URL, QUIRE_API_KEY, and UNLOCK_SECRET are
required; PORT defaults to 8787.
quire setup # prints the server .env block + a config example
quire publish --current # shape session, confirm, publish
quire publish --current --password random --expires tomorrow --yes
# agent path: random password (printed once),
# expires at next midnight, no confirmation prompt
quire list # list shares (id, title, created, expires, state)
quire revoke <token> --yes # hard delete (rows gone immediately), no prompt
quire update <token> --expires 2026-09-01
quire setup omp # installs the OMP /share handler (interactive TUI only)--password random (or generate/auto) generates a random secret and prints
it once. --expires accepts an ISO datetime, a duration (30m/24h/7d), or
a keyword (tomorrow, today, week, month, year, or in <duration>).
publish requires --current or a session id (there is no interactive
picker). update only changes the expiry — a share's password is fixed at
publish time (there is no update --password).
Environment: QUIRE_SERVER_URL, QUIRE_API_KEY (override the config file).
Harness detection: --harness zcode|claude-code|codex|omp flag, else the active
Codex/Claude Code/ZCode environment, else the most recently updated session
store. Codex reads ~/.codex/state_5.sqlite plus the selected task's rollout
JSONL, both read-only. Top-level tasks are discovered normally; a subagent task
can be published by passing its explicit task ID. OMP is never auto-detected:
publish an OMP HTML export by path with --harness omp.
quire setup omp installs Quire's custom share handler into OMP's agent
directory ($PI_CODING_AGENT_DIR, else ~/.omp/agent) as share.mjs. After
restarting or reloading OMP, /share in an interactive, persisted OMP TUI
session publishes the exact active conversation through Quire with strict
redaction, no password, and no expiry. The installer never overwrites or
chains an existing handler: if share.ts, share.js, or a different
share.mjs is present, it refuses and says what to rename or remove; a
re-run with matching bytes is a no-op. OMP does not fall back to its native
share when an installed handler fails, so to revert, rename or remove the
Quire-installed share.mjs and restart/reload OMP. Advanced options
(password, expiry, preset) are not part of OMP's argumentless /share;
publish the export directly with the desired flags instead. Headless and ACP
sessions, and --no-session runs, keep OMP's native behavior.
Install the plugin in Claude Code or ZCode and use /share. In Codex, install
the native plugin and use $share. Both infer password/expiration/preset from
what you ask and publish immediately (always --yes, no prompt), then report
the link — e.g. $share random password, expire tomorrow.
cp .env.example .env # set QUIRE_API_KEY, UNLOCK_SECRET, POSTGRES_PASSWORD
docker compose up -d --buildThe server listens on 127.0.0.1:8787 inside the host; front it with your
reverse proxy (Caddy/Nginx/Traefik) on your chosen host. Notes:
- Serve over HTTPS — the unlock cookie is
Secureand will not be sent over plain HTTP (localhost is exempt). - Forward
X-Forwarded-Forso rate limiting sees real client IPs. - The server runs its migrations at startup; no separate migration step.
- Unit:
pnpm --filter @quire/<server|cli|web|plugin> test(server tests needpnpm db:test:upfirst). - E2E:
pnpm --filter @quire/e2e test— builds and drives the compose stack on port 8790, then tears it down. Requires Docker.