Configuration and Storage
Spettro uses both project-local and user-global storage.
Global (~/.spettro/)
| Path | Purpose |
|---|---|
config.json |
Active provider/model, permission, token budget, auto-compact, favorites, UI state, local endpoints, thinking level, theme, workflow size and /ultra. |
keys.enc |
Encrypted API keys map by provider ID (see Encrypted API keys). |
keys.enc.v1 |
Backup of a pre-v2 keys.enc, written when it is migrated; kept for one release. |
master.key |
Random secret keys.enc is encrypted under (created on first use). |
trusted.json |
Permanently trusted project paths. |
catalog.json |
Cached provider/model catalog (see Model catalog). |
catalog-meta.json |
When the server last confirmed catalog.json, and the server's ETag/Last-Modified for it. |
update-check.json |
Result of the last GitHub release check, reused for 24 hours by the startup notice (/update always checks live). |
hooks.json |
Global runtime hooks fallback/default. |
lsp.json |
Optional LSP overrides; servers are auto-detected on PATH with zero config. |
bin/rg, bin/rg.version |
ripgrep, downloaded on first use when it is not on PATH (see Search tools); rg.version names the pinned release it came from. |
memory.md |
Persistent memory: user-scope facts loaded into agent context each session. |
memory-inbox.json |
Drafted memory candidates awaiting /memory review approval (never loaded into context). |
commands/ |
Global custom slash commands (.toml / .md prompt files). |
skills/ |
User Agent Skills (one folder with a SKILL.md per skill). |
history/<project-hash>/ |
Checkpointing shadow git repo and conversation snapshots (auto-created; reclaimable via /storage clean). |
sessions/<session-id>/ |
Session metadata, messages, tasks/todos, and agent events. |
conversations/<project-slug>/ |
Legacy conversation storage path kept for compatibility tooling. |
Encrypted API keys
keys.enc holds the provider API keys, encrypted with AES-256-GCM. The file
key is derived from the random secret in master.key (or from
SPETTRO_MASTER_KEY when that variable is set) and a per-file salt.
- Format v2 (
"kdf": "hkdf-sha256-v2"in the file) derives the key with HKDF-SHA256, which takes microseconds. Earlier versions used scrypt, which added about 60 ms to every start of the TUI, ACP and headless modes. The security does not change: the input is 32 random bytes, which a slow KDF cannot strengthen, andmaster.keysits next tokeys.enc. - Migration. The first start of a v2-capable build rewrites an older
keys.encin format v2 (temp file, fsync, rename, so a crash leaves the old or the new file, never a partial one). Before replacing it, the old file is copied byte for byte tokeys.enc.v1. - Downgrading. Older builds cannot read a v2 file. To go back, restore
the backup:
cp ~/.spettro/keys.enc.v1 ~/.spettro/keys.enc. Keys added after the upgrade are not in the backup and must be entered again. - Removal timeline.
keys.enc.v1is kept for one release: the release that introduces format v2 creates it, and the release after that stops creating it and deletes an existing copy. Reading older files stays supported, so an installation that skips a release still migrates. SPETTRO_MASTER_KEY. A passphrase set in this variable is not random, so files encrypted under it keep scrypt and are not migrated.
Model catalog
The model picker is built from the Spettro provider catalog
(catalog.spettro.app). Startup never waits for it:
- A cached copy in
~/.spettro/catalog.jsonis used when present. - Otherwise (first run, cache deleted, offline or behind a broken proxy) the snapshot embedded in the binary at build time is used.
- A background refresh then asks the server for a newer catalog only when
the server last confirmed the cached copy more than 6 hours ago. The
request is conditional: it carries the
ETagandLast-Modifiedthe server sent with that copy (kept incatalog-meta.json), so an unchanged catalog is not downloaded again. A cache without that file (written by an older build, or left behind afterspettro clean) is downloaded again once stale. - Long sessions re-check hourly under the same rule. The cache is shared by every spettro process: when another one (an ACP server, a second TUI) has downloaded a newer catalog, a running session switches to it at its next hourly check, without a request of its own.
Local endpoints (local_endpoints in config.json) and the Spettro
Subscription model list are also fetched in the background. Their models
appear in the TUI as each server answers. A result that arrives after you
removed or re-probed that endpoint (or signed out of the subscription) is
dropped, so it never undoes the change. If the configured model cannot run
(no key for its provider) and only a local endpoint can supply a
replacement, the TUI waits up to 2 seconds for the endpoints before it picks
and saves one. ACP session/new,
session/load and session/resume wait up to 2 seconds for them; a slower
server's models reach the editor afterwards as a config_option_update.
The headless server waits the same way before its first submission.
Project-local (<repo>/.spettro/)
| Path | Purpose |
|---|---|
PLAN.md |
Last generated implementation plan. |
allowed_commands.json |
Commands approved with “allow always” for this project. |
hooks.json |
Project runtime hooks (overrides global by (event, matcher, id)). |
lsp.json |
Optional project LSP overrides (wins over the global file per server key). |
memory.md |
Persistent memory: project-scope facts loaded into agent context each session. |
commands/ |
Project custom slash commands; override global commands on name conflict. |
skills/ |
Project Agent Skills; win over user skills of the same name. |
index.json |
Optional project snapshot when indexer-style flow is used. |
Project root
| Path | Purpose |
|---|---|
spettro.agents.toml |
Project-specific agent manifest (fallback is built-in default). |
Security model
- API keys are not stored in plaintext in
config.json. - Keys are encrypted with AES-GCM using a derived machine/user secret.
- Override key derivation input with
SPETTRO_MASTER_KEY. - First run in a folder requires explicit trust confirmation.
Permission levels
| Level | Behavior |
|---|---|
ask-first |
Strictest flow; approval-first execution model. |
restricted |
Allows execution with policy checks and approval gating where required. |
yolo |
Least restrictive execution policy. |
Theme
The TUI palette. See Themes for detection details and what the light palette re-tunes.
config.json key |
Default | Meaning |
|---|---|---|
theme |
"" (treated as auto) |
dark, light or auto. Written by /theme; an unrecognised value is cleared to the default on load. |
cursor_blink |
false |
Set true to make the input cursor blink. A blinking cursor repaints the screen twice a second while the TUI is open; the steady default leaves an idle TUI asleep. |
Precedence at startup is SPETTRO_THEME (environment, never persisted) >
theme in config.json > auto-detection > dark. auto seeds from COLORFGBG
before the first frame, then asks the terminal itself with an OSC 11 background
query and revises the palette when the answer arrives; it degrades to dark
whenever the background cannot be determined — including on a terminal that
cannot be asked at all, where COLORFGBG is an inherited value describing
whatever launched Spettro rather than Spettro's own terminal.
SPETTRO_THEME is a startup override, not a lock: /theme still repaints and
still saves while it is set, and the variable takes the palette back on the next
start.
SPETTRO_THEME=light spettro # override for one run
Provider wire
Which client carries streamed requests to OpenAI-compatible chat-completions
endpoints: catalog providers with an OpenAI-style API, the Spettro
Subscription and local servers. Anthropic-protocol providers, the official
openai provider (Responses API) and non-streamed requests always use the
fantasy SDK.
config.json key |
Default | Meaning |
|---|---|---|
provider_wire |
"" (treated as native) |
native: Spettro's own client, which re-encodes only the messages a step added and decodes streamed tool calls in linear time. fantasy: the fantasy SDK, as before the native client existed. An unknown value means native. |
SPETTRO_PROVIDER_WIRE (native or fantasy) overrides the key for one
process. Both clients send the same request JSON and report replies, errors
and rate limits the same way; the native one falls back to fantasy by itself
if it ever fails to encode a request. See Architecture.
The native client differs from fantasy only in these points, all but the last on replies where fantasy fails or loses data:
- a chunk whose delta carries both text and a tool-call fragment keeps both (fantasy drops the fragment);
- a choice without a
deltamember counts as an empty delta (fantasy fails the request); - a read error after the
[DONE]terminator is ignored (fantasy fails the request); - tool arguments that do not start as a JSON object, array or string stop being re-checked for completeness after 4 KB;
- requests carry the User-Agent
Spettro/<version>.
Like the SDK, the native client sends OPENAI_ORG_ID and
OPENAI_PROJECT_ID, when set, as the OpenAI-Organization and
OpenAI-Project headers.
Between the steps of a run the native client keeps each conversation's encoded messages (up to 48 conversations and 8 MB in all; a request of a single message is not kept) and the images it sent (up to 6 MB); both are released after 5 minutes without use.
Notifications
When the terminal is unfocused (or a run took more than 10 s), Spettro alerts
you on run/goal completion, on errors, and when the agent is waiting for a
command approval or an answer. Alerts go out on two channels at once: an OSC 9
terminal escape sequence — rendered as a system notification by iTerm2,
WezTerm, Ghostty, and Kitty; degraded to a terminal bell (BEL) elsewhere — and
a best-effort desktop notification (notify-send on Linux, osascript on
macOS).
config.json key |
Default | Meaning |
|---|---|---|
notifications_disabled |
false |
Set true to turn all notifications off. |
notify_quiet_sec |
5 |
Minimum seconds between notifications; events inside the window are dropped so bursts don't spam. |
Search
config.json key |
Default | Meaning |
|---|---|---|
ripgrep_download_disabled |
false |
Set true to never download ripgrep into ~/.spettro/bin; grep then uses rg only when it is on PATH and its built-in Go search otherwise. Read once per process (at the first grep without rg), so a change applies from the next start. See Search tools. |
Agent Skills
See Agent Skills for how skills are found and used.
config.json key |
Default | Meaning |
|---|---|---|
skills_compat_disabled |
false |
Set true to read skills only from .spettro/skills and ~/.spettro/skills, ignoring the Claude Code and Codex folders (.claude/skills, .agents/skills, .codex/skills, .openai/skills). |
disabled_skills |
[] |
Skill names hidden from the agent and the / menu; edited by /skill disable and /skill enable. |
Workflows
See Workflows for what the tiers mean, and
/ultra and ultracode for the ultra switch.
config.json key |
Default | Meaning |
|---|---|---|
workflow_size |
"" (treated as medium) |
small, medium, large or unbounded: the size guideline workflow runs plan around (about 5, 10 or 30 agents per workflow, or none). Written by /workflows size in the TUI and by the workflow_size selector or /workflow-size in ACP. A single run can override it with the tool's size argument. An unrecognised value is cleared to the default on load. |
ultra |
false |
The ultra switch, written by /ultra in the TUI and ACP and by the ACP Ultra toggle. While true, every turn runs in ultracode mode: substantive tasks run as dynamic workflows, as if the message contained the ultracode keyword. Suspended, not cleared, for any run whose effective permission is ask-first, because the workflow tool does not run there. In the TUI that is the active agent's level when permission is ask-first (so coding, restricted by its own spec, still engages); in ACP, headless and goal runs it is permission itself. |
Checkpointing storage
Shadow-git snapshot storage for /rewind; see Checkpointing
for how each key behaves.
config.json key |
Default | Meaning |
|---|---|---|
checkpointing_disabled |
false |
Set true to turn checkpointing (and /rewind) off entirely. |
checkpoint_max_file_mb |
20 |
Files larger than this are excluded from snapshots (recorded and surfaced on rewind). |
checkpoint_retention_days |
14 |
Checkpoints older than this are pruned when the shadow repo is opened. |
checkpoint_max_gb |
5 |
If the shadow store still exceeds this after retention, the oldest half of the remaining checkpoints is dropped. |
checkpoint_warn_gb |
2 |
One-time warning threshold for projects without their own .git (where snapshots must copy the tree). |
Storage cleanup
Session policy for /storage clean and spettro clean; see
Storage for the full artifact inventory.
config.json key |
Default | Meaning |
|---|---|---|
clean_session_age_days |
30 |
Sessions not updated within this many days become clean candidates. |
clean_keep_sessions |
5 |
The most recent K sessions per project always survive cleanup, regardless of age. |
Shell command approvals
- The
bashtool runs commands viabash -lc(PowerShell on Windows; see Windows). - Some safe commands are always allowed without a prompt:
ls,pwd,cat,head,tail,wc,grep,rg,stat,git status,git diff,go test/build/vetandmake test/build. The arguments are checked too: a flag that runs another program or writes a file (rg --pre,git diff --output/--ext-diff,go test -exec/-o/-coverprofile,make test SHELL=..., a leadingGIT_EXTERNAL_DIFF=...orGOFLAGS=...) sends the command through the normal approval path. - In non-
yolomodes, non-default commands require approval. - Choosing "allow always" stores normalized command approvals in
.spettro/allowed_commands.json. Normalizing only folds what a shell treats as one word break: a run of spaces and tabs outside quotes and not escaped by a backslash. Everything else is kept, so two commands share an entry only when a shell splits them into the same words (rm -rf ./x\ ~/andrm -rf ./x\ ~/, or a no-break space or vertical tab where the other has a space, are different entries).
Web access (web-search / web-fetch / download)
web-search,web-fetch, anddownloadare built-in network tools; see Web Tools for behavior, limits, and the HTML-to-markdown engine.- In non-
yolomodes each network target requires approval; "allow always" persists targets in.spettro/allowed_network.json. - All three go through the SSRF-hardened HTTP client: only http/https, non-public IPs (loopback, private ranges, cloud metadata) blocked at dial time, max 5 redirects.
downloadadditionally honors file-write approval and OS sandbox write roots; it never leaves partial files.
Commit co-authoring (mandatory)
- Every commit Spettro produces — directly via the built-in committer or indirectly when an agent runs
git committhrough thebashtool — carries the trailerCo-Authored-By: Spettro <spettro@eyed.to>. - The trailer is auto-injected by the runtime when missing. It is idempotent: if you (or the agent) already supplied the trailer, no second copy is added.
- Only the porcelain
git commitis rewritten; plumbing such asgit commit-treeis left untouched.
Runtime hooks
- Hook files are JSON and can be configured globally (
~/.spettro/hooks.json) and per-project (.spettro/hooks.json). - Supported events:
PreToolUse,PostToolUse,PermissionRequest,SessionStart. - Merge precedence: global rules load first; project rules override by
(event, matcher, id). - Hooks run via
bash -lcand may emit a JSON decision line:{"decision":"allow"}{"decision":"deny","reason":"..."}
updated_argsis honored forPreToolUseshell tools.
Agent manifest
Spettro loads spettro.agents.toml from the project root if present; otherwise it falls back to built-ins.
See AGENTS.md for full schema and validation. The
snippet below shows the root and [runtime] fields only; a complete manifest
also lists its [[tools]] and [[agents]]. Write the current version
(14): an older one is migrated, and the file rewritten with a .bak, on the
first load.
version = 14
default_agent = "plan"
[runtime]
default_permission = "ask-first"
default_timeout_sec = 120
sandbox_mode = "full-access" # off | read-only | workspace-write | full-access
log_tool_calls = true
# sandbox_net = "none" # all | localhost | none | ports:443,8080
# sandbox_allow_dirs = ["/data"] # extra writable roots inside the sandbox
# sandbox_allow_read_dirs = ["/opt/sdk"] # extra readable roots (e.g. toolchain caches)
OS sandbox
Spettro can confine the agent at the kernel level — Seatbelt (sandbox-exec) on
macOS, Landlock on Linux. It is opt-in and off by default; it complements
(never replaces) the approval gates. The boundary is invisible to the model:
there is no sandbox tool and no prompt advertisement, blocked operations look
like ordinary failures, and the policy is set only by the operator (CLI flag or
spettro.agents.toml) — the model cannot inspect it or request its way out.
Activation precedence: CLI flags > spettro.agents.toml > off.
spettro --sandbox workspace-write # writes -> workspace + temp; reads confined
spettro --sandbox read-only --sandbox-net none # no project/user writes, no network
spettro --sandbox-net ports:443 # TCP confined to port 443 (any host)
spettro --sandbox workspace-write --sandbox-allow-dir /data # extra writable root (repeatable)
spettro --sandbox read-only --sandbox-allow-read-dir ~/go/pkg/mod # extra readable root (repeatable)
What is guarded:
- Writes — confined identically for shell commands (kernel) and the
file-write/file-edittools (in-process check), soread-onlyis truly read-only and cannot be bypassed by writing through a file tool. Writable set: temp dirs, any--sandbox-allow-dirroots, and — inworkspace-writeonly — the workspace. - Reads — system paths stay readable so programs run, but the home tree
(other projects,
~/.ssh, credentials,~/.spettrokeys) is blocked except the workspace and any allowed roots. Toolchain caches that live in$HOME(e.g.~/go/pkg/mod,~/.cache) are blocked too, so add them with--sandbox-allow-read-dirwhen building under the sandbox. - Network —
nonedenies it,localhostallows loopback only,ports:443,8080allows those TCP ports (any host). The spettro process keeps network access for the LLM API, so the agent always reaches its model even undernet=none. - The spettro process itself — write-confined as defense-in-depth (Landlock
in-process on Linux; a one-time
sandbox-execre-exec on macOS). Its reads stay open so the in-process git committer and skill discovery keep working; the model's own read surface is confined at the shell and file-tool layers.
Platform caveats:
- Linux network confinement needs Landlock ABI v4 (kernel 6.7+) and governs TCP
only (UDP/unix sockets pass);
localhostdegrades to deny-all TCP because Landlock cannot scope rules to loopback. If the kernel cannot enforce a requested policy, sandboxed commands fail closed (exit 126). - macOS network filters are ip/port based (no hostname allowlists). Under
none, DNS and unix sockets are blocked too. - macOS parent confinement uses a
sandbox-execre-exec rather than the deprecated in-processsandbox_init, to keep theCGO_ENABLED=0cross- compiled release builds working.
