Skip to content

Architecture

How the pieces fit, in 10 minutes. Read this if you want to extend Strata, debug a behaviour, or just understand what’s happening under the hood.

Three durable artefacts: the vault (markdown, human-readable), the FTS index (machine, regeneratable), and the code graph (machine, regeneratable).

Two interaction surfaces: skills (Claude reads markdown, follows instructions, executes bash) and MCP tools (Claude calls read-only tools for structured retrieval).

.claude-plugin/
├── plugin.json # name, userConfig schema
└── marketplace.json # self-referential, `/plugin marketplace add ./` works
hooks/
└── hooks.json # SessionStart, PostToolUse, PreCompact, Stop
mcp/
├── .mcp.json # stdio server config
└── server.py # 7 registered tools, plus internal helpers; 1 resource surface
skills/<name>/SKILL.md # one folder per skill, frontmatter declares triggers
agents/<name>.md # subagent definitions, e.g. bootstrap-worker
scripts/ # the Python that actually does things
bin/
├── bootstrap-venv.sh # creates .venv, installs requirements
└── run-python.sh # wrapper used everywhere; resolves userConfig

The plugin auto-discovers skills (any directory under skills/), agents (any .md under agents/), and hooks (hooks/hooks.json). No registration step.

When Claude Code launches a script, it exports plugin config as CLAUDE_PLUGIN_OPTION_<name> env vars. The bin/run-python.sh wrapper does 4-layer resolution:

  1. STRATA_* shell env (user override)
  2. CLAUDE_PLUGIN_OPTION_* (Claude Code auto-export)
  3. ~/.claude/settings.json pluginConfigs lookup (manual override)
  4. Script default

So STRATA_VAULT_PATH=/tmp/test-vault python script.py works for one-shot testing; the production path is CLAUDE_PLUGIN_OPTION_VAULT_PATH from Claude Code.

Every script that reads or writes inside the vault uses lib.safe_resolve(rel, root). The helper:

  • Rejects absolute paths
  • Rejects .. traversal
  • Rejects symlinks anywhere along the resolved path
  • Rejects anything resolving outside root

This is the only protection against malicious or accidental path escape. Tested in tests/test_sandbox.py.

A skill is instructions to Claude in markdown. A script is Python that does work.

Skills are how Claude knows what to do when a user expresses an intent. Scripts are how the work gets done. The pattern:

  1. User says something matching the skill’s description: frontmatter
  2. Claude loads SKILL.md, follows the instructions
  3. Instructions tell Claude to run a script via Bash (visible in the user’s terminal)
  4. Script writes a note, refreshes the index, etc.

This split is deliberate. Skills can change without code changes. Scripts can change without skill rewrites. The two evolve independently.

/strata:bootstrap doesn’t process files inline. It dispatches strata:bootstrap-worker subagents in parallel batches. Each worker:

  • Has its own context window (main agent context stays clean)
  • Has restricted tools (Read, Write, Bash, Glob, Grep; no Agent, no WebSearch, no Edit)
  • Has no MCP grant, so it runs the bundled scripts via Bash for context (plan_correlate is a script, not a tool)
  • Reads a group of related source files
  • Writes one note per concept (or skips)
  • Returns one summary line per write

The grouping (by parent directory) prevents the duplicate-ADR problem an earlier per-file design produced.

mcp/server.py uses the official mcp Python SDK with stdio transport. ~800 lines, 7 registered tools plus internal helpers, and resource registration for the vault as strata://<scope>/<filename> URIs.

Started by Claude Code via mcp/.mcp.json pointing at bin/run-python.sh mcp/server.py. Lives for the lifetime of the session.

Tools are dispatched through a single call_tool handler. Adding a new tool: add to list_tools() + add an if name == "..." branch in call_tool. See MCP tools.

.strata/index.db (per repo, in the project dir, not the vault, to avoid polluting Obsidian sync) holds:

  • files table — one row per indexed note (path, status, kind, scope, branch, indexed_at)
  • fts virtual table — FTS5 over title + body
  • supersedes — ADR supersession edges
  • links — wikilink graph

Regenerated by scripts/refresh-index.py, which any write skill calls after writing. The index is disposable. Delete it and the next read regenerates from the vault.

FieldWhereMeaning
titleallHuman-readable name
statusallstable, proposed, accepted, superseded, invalidated, draft
kindalldecision, domain, lesson, session, handoff, etc.
source_filebootstrap-extractedProject-relative provenance
code_refsextractedList of symbols this note references (verified against graph)
correctionsedited via /strata:correctAudit log of changes
supersedes / superseded_bydecisionsADR lineage
invalidated_at / invalidated_by / invalidation_reasoninvalidatedAudit

Parsed by python-frontmatter. The index extracts the structured fields; the body stays untouched.

Adding a new skill: drop a skills/<name>/SKILL.md with appropriate frontmatter. Skill auto-loads on next session.

Adding a new MCP tool: register in list_tools() + add if name == "..." dispatch branch.

Adding a new agent: drop agents/<name>.md with YAML frontmatter (name, description, model, tools). Auto-registers as <plugin>:<name>.

Adding a new lint preset: presets/<name>.json with the schema from presets/secrets.json.

The plugin manifest doesn’t need updating for any of these. Auto-discovery handles registration.

docs/ ← you're reading this
scripts/ ← the work happens here
├── lib.py (vault paths, safe_resolve, helpers)
├── db.py (SQLite FTS5)
├── code_graph.py (Graphify integration)
├── doc_claims.py (path/symbol regex extraction)
├── bootstrap-scan.py (candidate enumeration)
├── plan_correlate.py (claims vs git history)
├── new-decision.py, save-note.py, correct-note.py, invalidate-note.py
└── ...
skills/ ← Claude reads these
agents/ ← subagents for fan-out work
mcp/server.py ← Claude calls these tools
hooks/hooks.json ← SessionStart, Stop, PreCompact, PostToolUse
tests/ ← 170+ tests; pytest from repo root

If you want to know how something works, find the script in scripts/ and read it. They’re terse Python with explanatory docstrings; the design lives in the code, not in framework abstractions.