Skip to content

Trust model

threadle is a local tool that reads other tools’ session storage and drives their CLIs. Its threat model is a single-user localhost app: the adversary is a web page in your browser, a malicious node folder, or a format change upstream — not a remote attacker, because there is no remote surface.


Session data is parsed, never modified.

ProviderStorage threadle reads
Claude Code~/.claude/projects — JSONL transcripts
opencode~/.local/share/opencode/opencode.db — SQLite
Cursor~/.cursor/projects — transcripts and chat metadata

The database is opened read-only and the JSONL files are only ever read. Parsers skip records they do not recognize rather than throwing — these formats are undocumented and churn, and a new message kind upstream must not break your workflow list.

Agent runs go through each tool’s official CLI. Those tools then write their own session storage exactly as they would if you had invoked them from a terminal yourself — that writing is theirs, not threadle’s.


Everything threadle persists lives under ~/.config/threadle. Settings → threadle internals lists it item by item with sizes, and marks which items are safe to clear.

PathContentsClearable
settings.jsonEditor, pricing source, preferencesno
graphs/Your workflows and sub-workflows, one JSON eachno
nodes/Custom nodesno
skills/custom, skills/importedSkillsets you authored or importedcustom: no
payloads/Content-addressed context payloadsyes
search.dbSQLite full-text index over transcripts and payloadsyes
runs/jobs.jsonl, runs/logs/Run history and per-run logsyes
runs/injects.jsonlLineage — which context went whereyes
models-pricing.jsonCached model price listyes
tmp/Scratch files written while injecting contextyes

The only files threadle writes outside that directory are the ones an agent CLI creates in your project when you run it there.


The server binds 127.0.0.1 on port 4570 (--port to change it). There is no bind-to-0.0.0.0 flag, no auth token, and no remote mode — because nothing is listening off-loopback.

Loopback-bound is not the same as safe from browsers, so two guards sit in front of every request:

Host guard (DNS rebinding). A hostile site can point its own domain at 127.0.0.1 and then treat threadle as same-origin. Any request whose Host header is not 127.0.0.1, localhost, or [::1] (with optional port) is rejected with 403 forbidden host.

Origin guard (CSRF). Browsers attach Origin to cross-site and same-origin writes alike. Any non-GET/HEAD/OPTIONS request carrying an Origin from another host is rejected with 403 cross-origin write rejected. A request with no Origin at all is curl or the CLI, which is fine on loopback.

Every response also carries:

Content-Security-Policy: default-src 'self'; script-src 'self';
style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self';
font-src 'self'; object-src 'none'; base-uri 'none';
frame-ancestors 'none'; form-action 'self'
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: no-referrer
Cross-Origin-Resource-Policy: same-origin

frame-ancestors 'none' plus X-Frame-Options: DENY means the UI cannot be framed; connect-src 'self' means the page cannot phone anywhere. Request bodies to /api/* are capped at 20 MB.

Two things, both of them yours:

  1. The agent CLIs you invoke. claude, opencode, and Cursor’s agent talk to their own backends under your own credentials, as they always do.
  2. One price list. Model prices come from a local copy first — opencode’s ~/.cache/opencode/models.json, then threadle’s own cache — regardless of age. https://models.dev/api.json is fetched only when no local copy exists at all, or when you press refresh in Statistics. The URL is configurable in settings.

No telemetry, no analytics, no update check, no crash reporting.


Custom nodes are code you placed on disk, so running them is the same trust level as your shell. threadle still fences them in:

GuardrailDetail
No shellcommand is an argv array. There is no shell, so no interpolation, globbing, or injection through node input.
Working directoryPinned to the node’s own folder.
Isolated processEvery run is a fresh child process — a crashing or hanging node cannot take the server with it.
TimeouttimeoutMs, default 60 000 ms, hard-capped at 300 000 ms.
Output cap4 MB of process output; beyond that the run fails.
ConcurrencyAt most 8 custom-node processes at once.
Environment allowlistOnly PATH, HOME, LANG, LC_ALL, TMPDIR, TERM, SHELL, plus THREADLE_NODE and the node’s own params. The server’s environment — API keys included — is not inherited.
Explicit opt-outA descriptor may declare "env": "inherit" to get the full parent environment. It is one visible line in node.json, surfaced as a decision you made.
Metadata is sandboxed tooFor a node with no node.json, class metadata is extracted by a separate child process. User code never executes inside the threadle server, not even at scan time.
Import executes nothingCloning, copying, or uploading a node runs none of its code. It runs when you wire it and press Run.

Uploaded folders are additionally path-sanitized (no absolute paths, no ..), skip .git and node_modules, and are capped at 200 files / 2 MB per file / 10 MB total.


  • Never mutates ~/.claude/projects
  • Never mutates the opencode SQLite database
  • Never mutates Cursor transcript files
  • Never installs, updates, or executes a custom node on its own
  • Never sends your transcripts, prompts, or outputs anywhere