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.
The layers
Section titled “The layers”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).
Plugin manifest
Section titled “Plugin manifest”.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 triggersagents/<name>.md # subagent definitions, e.g. bootstrap-worker
scripts/ # the Python that actually does thingsbin/├── bootstrap-venv.sh # creates .venv, installs requirements└── run-python.sh # wrapper used everywhere; resolves userConfigThe plugin auto-discovers skills (any directory under skills/), agents (any .md under agents/), and hooks (hooks/hooks.json). No registration step.
userConfig resolution
Section titled “userConfig resolution”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:
STRATA_*shell env (user override)CLAUDE_PLUGIN_OPTION_*(Claude Code auto-export)~/.claude/settings.jsonpluginConfigslookup (manual override)- 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.
Path safety
Section titled “Path safety”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.
Skills vs scripts
Section titled “Skills vs scripts”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:
- User says something matching the skill’s
description:frontmatter - Claude loads SKILL.md, follows the instructions
- Instructions tell Claude to run a script via Bash (visible in the user’s terminal)
- 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.
Subagent dispatch
Section titled “Subagent dispatch”/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_correlateis 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
Section titled “MCP server”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.
SQLite FTS index
Section titled “SQLite FTS index”.strata/index.db (per repo, in the project dir, not the vault, to avoid polluting Obsidian sync) holds:
filestable — one row per indexed note (path, status, kind, scope, branch, indexed_at)ftsvirtual table — FTS5 over title + bodysupersedes— ADR supersession edgeslinks— 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.
Frontmatter conventions
Section titled “Frontmatter conventions”| Field | Where | Meaning |
|---|---|---|
title | all | Human-readable name |
status | all | stable, proposed, accepted, superseded, invalidated, draft |
kind | all | decision, domain, lesson, session, handoff, etc. |
source_file | bootstrap-extracted | Project-relative provenance |
code_refs | extracted | List of symbols this note references (verified against graph) |
corrections | edited via /strata:correct | Audit log of changes |
supersedes / superseded_by | decisions | ADR lineage |
invalidated_at / invalidated_by / invalidation_reason | invalidated | Audit |
Parsed by python-frontmatter. The index extracts the structured fields; the body stays untouched.
Extending
Section titled “Extending”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.
What lives where (quick map)
Section titled “What lives where (quick map)”docs/ ← you're reading thisscripts/ ← 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 theseagents/ ← subagents for fan-out workmcp/server.py ← Claude calls these toolshooks/hooks.json ← SessionStart, Stop, PreCompact, PostToolUsetests/ ← 170+ tests; pytest from repo rootIf 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.