Skip to content

SpecStory internals — filename algorithm, reverse lookup, markdown structure

Reference for what SpecStory writes to .specstory/history/*.md and how this repo's tv agent-sessions channel links live coding-agent sessions back to those files. For install / config / providers, see SpecStory CLI.

This page documents the output format, not the runtime. Read it when you want to:

  • Map a live session id (from claude --resume / codex resume / cursor-agent --resume) back to its .specstory/history/*.md.
  • Grep SpecStory transcripts programmatically (agent, model, timestamps are all plain-text markers).
  • Re-render a SpecStory transcript elsewhere (the structure is intentionally simple).

The SpecStory CLI is Go, BSD 3-Clause, at https://github.com/specstoryai/getspecstory. DeepWiki has a generated technical reference at https://deepwiki.com/specstoryai/getspecstory that's also useful for orientation.

Filename algorithm

Format: YYYY-MM-DD_HH-MM-SS[Z|±HHMM]-<slug>.md

Verified empirically against the files in this repo's .specstory/history/:

Example Notes
2026-04-24_09-09-09Z-commit-commit-backup-e.md current default (UTC, with seconds)
2026-03-30_11-20-14+0800-utility-function-bash-hash.md --local-time-zone mode (non-UTC offset)
2026-02-10_10-41Z-git-commit-history-modification.md older format (UTC, no seconds) — prior CLI version, not retroactively renamed
2026-04-24_06-54-14Z.md empty slug — no qualifying first user message

Timestamp source: RFC3339 parse of the session's CreatedAt — the timestamp of the first JSONL / SQLite record SpecStory sees, which is typically the first user message. UTC is the default; --local-time-zone switches to a numeric offset.

Slug: first 4 words of the first real (non-synthetic) user message, after:

  1. Strip XML context tags injected by IDEs (<ide_opened_file>, <environment_context>, etc.).
  2. Unicode NFD normalization + accent stripping; lowercase.
  3. @at, &and, #hash.
  4. Strip remaining punctuation; split on whitespace.
  5. Take first 4 words, join with -.

Empty slug (no qualifying user message yet) produces a filename with just the timestamp.

Session id is NOT in the filename. It's embedded as an HTML comment on line 5 of the body — see the next section.

Source pointers (as of current dev branch):

Responsibility Package / function
Filename timestamp format pkg/session/session.goFormatFilenameTimestamp
Final path assembly pkg/session/session.goBuildSessionFilePath
Slug from first user message pkg/spi/path_utils.goGenerateFilenameFromUserMessage
Claude-specific slug pull pkg/providers/claudecode/provider.goFileSlugFromRootRecord
Markdown body generation pkg/outputgen/GenerateMarkdown / writeMarkdownToFile

Collision handling:

  • Byte-identical content on disk → write is skipped ("up to date").
  • Timestamp + slug collide with different content → second write overwrites. Undocumented; rare in practice (requires two sessions starting in the same second with the same first-4-words).

Historical files are not migrated. If the format evolves (e.g. seconds were added at some point), existing .specstory/history/*.md stay as-is.

Reverse lookup — session id → markdown file

Every file starts with a fixed preamble (line 5 is the key line):

<!-- Generated by SpecStory, Markdown v2.1.0 -->

# YYYY-MM-DD HH:MM:SSZ

<!-- <Provider-display-name> Session <uuid> (YYYY-MM-DD HH:MM:SSZ) -->

Provider display names observed in this repo's .specstory/history/:

In HTML comment Provider ID used for specstory run <id>
Claude Code claudecode
cursoride (Cursor IDE composer chat) — (written by Cursor IDE extension, not CLI)
Codex codexcli
OpenCode (not yet — see backlog/specstory-opencode-support.md)
Gemini geminicli
(not seen here) Droid droidcli
(not seen here) Cursor CLI cursorcli

The UUID in the <!-- ... Session <uuid> ... --> line is the same session id the agent uses for its resume flag (claude --resume <uuid>, codex resume <uuid>, cursor-agent --resume <uuid>, opencode --session <uuid>). That makes session id → transcript file a one-liner grep, uniform across providers:

grep -l "Session ${sid}" .specstory/history/*.md

SpecStory CLI has no built-in resolve <sid> → path subcommand. The documented commands are specstory run / specstory sync / specstory watch. The grep recipe above is the supported path.

For the tv agent-sessions channel's integration, see dot_config/television/executable_agent-sessions.py — it caches the map on startup from ~/.local/share/chezmoi/.specstory/history/ and exposes the matched path as TSV column 5 (bound to Alt+S to copy).

Markdown body structure

Rough skeleton written by SpecStory (pkg/outputgen/):

<!-- Generated by SpecStory, Markdown v2.1.0 -->

# 2026-04-24 09:09:09Z

<!-- Claude Code Session <uuid> (2026-04-24 09:09:09Z) -->

_**User (2026-04-24 09:09:09Z)**_

<first user prompt>

---

_**Agent (claude-opus-4-7 2026-04-24 09:09:13Z)**_

<think><details><summary>Thought Process</summary>
<thinking-block text>
</details></think>

<assistant reply prose>

<tool-use data-tool-type="shell" data-tool-name="Bash"><details>
<summary>Tool use: **Bash**</summary>
<tool invocation JSON + output>
</details></tool-use>

---

_**User**_

Key elements:

  • Role headers are italic-bold with optional model + timestamp: _**User (ts)**_, _**Agent (<model> <ts>)**_.
  • --- separates turns.
  • Thinking blocks are collapsed <details> wrapped in a <think> tag.
  • Tool use is collapsed <details> inside a <tool-use> tag with data-tool-type and data-tool-name attributes.
  • No fenced code blocks around tool I/O — the <details> payload is raw text / JSON.

glow and bat both render this cleanly. <think> and <tool-use> are non-standard HTML elements; in glow's HTML passthrough they surface as collapsible sections.

Caveats

  • Cloud-sync failures never touch local files. SpecStory writes markdown locally first; a failing cloud sync only stops the upload.
  • --only-cloud-sync skips local writes entirely. If you use that flag, .specstory/history/ won't grow — the reverse-lookup table will be empty.
  • --no-cloud-sync is the opposite — local writes proceed, cloud sync is disabled.
  • --debug-raw dumps raw session records to .specstory/debug/ alongside the markdown — useful if a transcript looks wrong and you want to see exactly what SpecStory saw.
  • Provider IDs vs display names are not the same strings. specstory run claudecode is the CLI ID; the HTML comment in the resulting markdown reads Claude Code (with space). If you script against either string, pick the right one — the HTML comment is what the reverse-lookup grep matches.

References