taleseal
v0.6.0
Published
Seal an agent run as a shareable tale — taleseal CLI
Readme
taleseal
Seal a local Claude Code, Codex or Cursor session as a published tale — the shareable narrative of one run: what triggered it, what it did, what came out, with the receipts to verify it. Loom for agent runs.
npx taleseal seal --preview # what would be published — nothing leaves your machine
npx taleseal seal # publish; get back a link for the PR or Slack threadseal finds the newest Claude Code session for your current directory and composes the tale
locally. The first publish signs you in on the spot — login opens the browser, creates an
account if you need one, and saves the key on your machine.
Login
Bare taleseal login opens the browser: approve there — signing up on the way if needed —
and an API key is created and stored in ~/.config/taleseal/config.json
($XDG_CONFIG_HOME respected, file mode 0600). It works from SSH boxes and containers
too: open the printed URL on any device and match the confirmation code. Codes last ten
minutes and work once.
For CI, scripts and browserless machines, taleseal login --key tk_… stores a key minted
by hand in the dashboard instead. It is validated against
the server first: a key rejected with 401 is never stored; if the server is unreachable the
key is stored anyway, with a warning. taleseal logout removes the stored key.
Usage
taleseal seal [--transcript <path>] [--agent <name>] [--status <status>]
[--publisher <name>] [--transcript-url <url>] [--preview]
[--yes] [--dry-run] [--json]
taleseal login [--key <tk_…>]
taleseal logout| Flag | Meaning |
| --- | --- |
| --transcript <path> | Transcript JSONL to seal. Omitted → newest *.jsonl in ~/.claude/projects/<this directory>/ |
| --agent <name> | Agent name shown on the tale (default: taken from the transcript format) |
| --status <status> | Outcome status recorded on the tale: succeeded | partial | failed (default succeeded) — a tale is allowed to be an honest failure report |
| --publisher <name> | Recorded as provenance.publisher — who ran this / under whose authority |
| --transcript-url <url> | Recorded as provenance.transcriptUrl — a link to the full raw session. Must be an http(s) URL; local paths are rejected |
| --preview | Print the human-readable pre-publish preview (title, status, beats, receipts, redaction report) to stdout and exit 0 — no key needed, nothing leaves your machine. This is what plugins show before asking "publish?" |
| --yes | Skip the pre-publish redaction gate prompt. Required when stdout is not a TTY (hooks, CI, pipes) |
| --dry-run | Print the composed tale JSON to stdout — nothing leaves your machine |
| --json | On publish, print {"id","url"} instead of the bare URL |
On success the tale URL is printed to stdout. Errors go to stderr with exit code 1.
The redaction gate
Secrets travel nowhere without a human seeing the redaction report. When seal is about to
publish (i.e. not --dry-run), it first builds a redaction report — how many
[REDACTED:<kind>] markers the scrubber left in the tale, counted by kind, with up to three
sample matches shown in their (already-redacted) context. Then:
- Interactive terminal: a compact preview (title, status, beats, evidence count, the
redaction report) is printed and you are asked
Publish? [y/N]. Anything but yes cancels; nothing leaves the machine. - Not a TTY (git hooks, CI, piped output): publishing is refused with exit code 1 unless
--yesis passed. Automation must opt in explicitly to publishing unseen. --yesskips the prompt in both cases.
taleseal seal --preview prints exactly this preview to stdout and exits without
publishing, so you (or a plugin) can see the report before deciding anything. --dry-run
is unchanged: it prints the tale JSON and never gates.
Configuration
Both values resolve in the same order everywhere — env var first, then the config file
written by taleseal login, then the default:
| Value | Resolution order |
| --- | --- |
| API key | TALESEAL_API_KEY env var → config "apiKey" |
| Base URL | TALESEAL_URL env var → config "url" → https://taleseal.com |
The config file is ~/.config/taleseal/config.json ($XDG_CONFIG_HOME/taleseal/config.json
when set), shape {"apiKey": "tk_…", "url"?: "https://…"}, mode 0600. A key is required to
publish; --preview and --dry-run need none.
Supported agents
| Agent | Transcript source | Status |
| --- | --- | --- |
| Claude Code | ~/.claude/projects/<project>/<session>.jsonl | ✅ |
| Codex | ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl | ✅ |
| Cursor | ~/.cursor/projects/<project>/agent-transcripts/<session>/<session>.jsonl (--cursor, or the hook's $CURSOR_TRANSCRIPT_PATH) | ✅ |
What the composed tale contains (format v2)
Composition is fully local and deterministic — no LLM pass, no telemetry. Beyond the v1 narrative (title, trigger, beats, metrics), a v2 tale carries:
outcome.status—succeeded/partial/failed, from--status.provenance— auto-filled:toolis the harness that ran the work (claude-code,codexorcursor, with the version when the transcript records one),extentisfully_ai,publisherandtranscriptUrlcome from their flags.- Beat
ids — stable slugs generated from each beat's title (deduplicated), so beats can be deep-linked. evidence— receipts extracted deterministically from the transcript's tool calls and attached to the beat they occurred in (at most 6 per beat; overflow is noted in the prose):- shell commands →
commandevidence (command + captured output excerpt + exit code when the transcript records one); - test-runner commands (
bun test,jest,vitest,pytest,go test, …) →testevidence, withpasseddecided only by explicit output markers ("12 pass", "0 fail" vs "fail"/"error") — undecidable runs stay plaincommandevidence, never a claimed result; - edit/write calls → one aggregated
diffevidence per beat ("N files touched" + the file list when knowable); - web/search calls with a URL →
citationevidence.
- shell commands →
Before any string enters the tale it is scrubbed client-side: AWS access keys, GitHub/Slack
tokens, sk- API keys, JWTs, Bearer headers, PEM private-key blocks and
password=/secret:-style assignments are replaced with [REDACTED:<kind>] markers —
before truncation, so a secret can never be split into an unrecognisable half.
A tale carries the whole run. Every step becomes a beat and every receipt is kept — nothing is folded away for length. A tale exists to show the evidence behind what an agent did, and the evidence a reader came to check (the test that failed, the diff, the command that proved it) lives in the middle of a long session, exactly where a digest would cut.
Anyone holding the published URL can view the tale; the redaction gate above exists so you see exactly what that is before it ships.
Programmatic API
import { buildRedactionReport, composeTale, decideGate, readTranscript, redact } from "taleseal";
import { publishTale } from "@taleseal/sdk";
const run = await readTranscript("/path/to/session.jsonl");
const tale = composeTale(run, { agent: "release-bot", status: "succeeded", publisher: "goran" });
const report = buildRedactionReport(tale); // counts by kind + sample matches in context
await publishTale(tale); // reads TALESEAL_API_KEY; TALESEAL_URL defaults to https://taleseal.comRequires Node ≥ 22.
