trail-sh
v0.1.0
Published
The flight recorder for agent runs. A durable, multi-writer, append-only run log that is just a URL.
Maintainers
Readme
The one-liner
curl -d "migration started" https://trail.legible.sh/swift-otter-9921That topic now exists. Open https://trail.legible.sh/swift-otter-9921 in a browser and it is a live journal — dark, monospace, auto-following. curl it and it is plain text. Tomorrow it is still there. Paste it into the PR.
The hosted API at trail.legible.sh is in soft launch. To run the identical API yourself,
npx trail-sh servestarts it locally; every example below works withhttp://127.0.0.1:4182in place ofhttps://trail.legible.sh.
Why
You kick off a long agent run — a migration, a fleet of workers, a Claude Code session on a box you are about to walk away from — and the record of what happened lives in a scrollback buffer that dies with the terminal. Screen-sharing tools mirror the session but can't be handed to a teammate, don't merge five workers into one story, and vanish when the session does. trail is the other thing: not a window into the terminal, but a paper trail. Any process appends with one curl. Everyone reads from the same URL. It is still there tomorrow.
The whole API
| Request | What it does |
| --- | --- |
| POST /{topic} | Append. Each newline-separated line in the body becomes an entry. Optional headers X-Level: info\|warn\|error\|done, X-Actor: worker-3 (or ?level=&actor=). Returns {"seq": 7} — the last assigned sequence number. |
| GET /{topic} | The journal. Browsers get the live HTML page (SSE follow, auto-scroll, level colors, actor labels). curl gets plain text, one line per entry: at level actor \| text. Accept: application/json gets JSON. |
| GET /{topic}.txt | Always plain text, for piping. |
| GET /{topic}.json | Always JSON: {"topic", "entries": [{seq, at, actor, level, text}]}. |
| GET /{topic}/sse?since=SEQ | SSE stream: named entry events (JSON data, id: = seq), a deleted event if the journal is removed, heartbeat comments every 25s. since=0 replays the full backlog, then follows. Also served on GET /{topic} with Accept: text/event-stream. |
| DELETE /{topic} | Delete the journal (needs the token if the server has one). |
| GET /README.md, GET /llms.txt | This document (text/markdown) and a machine-readable index — never token-gated. GET / with Accept: text/markdown also returns the README. |
Query params on reads: ?n=100 (last N), ?since=SEQ (entries after SEQ), ?wait=30 (long-poll: hold up to 30s for the next entry). Topics match [a-zA-Z0-9_-]{1,64} and are created by first write. Errors are JSON: {"error": "...", "code": "...", "hint": "..."} — the hint spells out the correct next request.
Teach your agent
Paste this into your CLAUDE.md / AGENTS.md and your agent knows the whole tool:
# trail — append-only run journal, plain HTTP (https://trail.legible.sh, or self-hosted; set base URL accordingly)
# Append one or more lines (topic is created on first use; pick an unguessable name):
curl -d "build started" https://trail.legible.sh/TOPIC
# Optional headers: X-Level: info|warn|error|done and X-Actor: worker-3
curl -d "tests red" -H "X-Level: error" -H "X-Actor: ci" https://trail.legible.sh/TOPIC
# Read back: plain text, last N, or JSON:
curl https://trail.legible.sh/TOPIC.txt?n=100
curl https://trail.legible.sh/TOPIC.json
# Follow live (SSE), or long-poll for entries after seq 42:
curl -N https://trail.legible.sh/TOPIC/sse?since=0
curl "https://trail.legible.sh/TOPIC.json?since=42&wait=30"
# Humans watch the same URL in a browser. Log key events, not every token.
# Finish runs with: curl -d "exit 0" -H "X-Level: done" https://trail.legible.sh/TOPICWatch a Claude Code session from your phone
examples/claude-hooks.sh wires Claude Code's PostToolUse and Stop hooks to a topic — one line per tool call, a done line when the session ends. Open the topic URL on your phone; that's the whole integration. No account, no app, no tunnel. Run ./examples/claude-hooks.sh demo against a local server to see the page with fake data first.
Self-hosting
npx trail-sh serve # http://0.0.0.0:4182, in-memory
trail serve --data-dir ./trail-data # durable: JSONL per topic, replayed on boot
trail serve --token s3cr3t # writes/deletes need Authorization: Bearer s3cr3t
trail serve --port 8080 --host 127.0.0.1 --base-url https://trail.example.comOr clone and run: git clone https://github.com/legible-sh/trail && cd trail && npm start. There is nothing to install — zero dependencies, Node 20+.
Without --data-dir, journals live in memory and die with the process. With it, every entry is appended to <data-dir>/<topic>.jsonl and replayed on boot. Either way the last 10,000 entries per topic are served; older ones are evicted from memory (all limits are constants in src/limits.mjs).
CLI
The CLI is sugar over the same HTTP API — curl remains the contract. It targets --url, else $TRAIL_URL, else https://trail.legible.sh; --token / $TRAIL_TOKEN add the bearer token.
trail log deploy-7 "smoke tests green" --level done --actor ci
trail tail deploy-7 # follow live in the terminal (--since SEQ)
trail pipe deploy-7 -- npm test # run a command, tee every output line into the journal
make build 2>&1 | trail pipe deploy-7 # or tee stdin
trail serve # run a servertrail pipe records stdout as info, stderr as warn, and finishes with an exit N entry — done for zero, error otherwise — while passing everything through, so it drops into existing pipelines.
Pro
Planned for the hosted instance, never gating the core verbs (self-host stays complete):
- Retention beyond 7 days / 10,000 lines per topic.
- Read tokens for genuinely private journals (today privacy = unguessable names).
- Full-text search across your topics — "which run touched
users.sql?" - Orgs: reserved topic prefixes, per-seat pricing, one bill.
Straight talk
- Unguessable names are the access model. Anyone with the URL can read, and (without
--token) write. That is capability-by-obscurity — the same honest deal as ntfy.sh. Don't log secrets; use--tokenand a private network when it matters. - Reads are never token-gated. The shareable journal URL is the product, so
--tokenprotects writes and deletes only. If your logs themselves are sensitive, self-host somewhere private — read tokens are on the Pro path. --data-dirfiles grow until you DELETE. Memory serves the last 10,000 entries per topic; the JSONL file underneath keeps everything until the journal is deleted. Rotate by deleting or by starting a new topic per run (recommended anyway).- Ordering is per-server arrival order. Sequence numbers are assigned on arrival at one server; there is no distributed clock and no cross-topic ordering.
- No WebSocket. SSE, long-poll, and one-shot GET cover every client we've met, and the zero-dependency constraint is worth more than a fourth transport.
The family
trail is one of the legible primitives, each a single npm package honoring the same principles: the URL is the API, curl is the SDK, zero ceremony.
| | port | | | --- | --- | --- | | gate | 4180 | ntfy tells you things. gate asks you things. | | bigred | 4181 | The big red button for your agent fleet. | | trail | 4182 | The flight recorder for agent runs. | | slate | 4183 | The blackboard from the multi-agent papers, as a URL. | | relay | 4184 | The work queue your agents can provision themselves. | | mutex | 4185 | flock(1) for agents that live on different machines. | | quorum | 4186 | Coordination for agents that do not share a parent process. | | meter | 4187 | The kill-brake for agent spend. 429-as-a-service. | | stash | 4188 | ntfy moves signals. stash moves bytes. | | tally | 4189 | StatHat reborn as ntfy. Three months too late — or right on time. |
License
MIT
