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.
Read-only against agent storage
Section titled “Read-only against agent storage”Session data is parsed, never modified.
| Provider | Storage 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.
Writes: one directory
Section titled “Writes: one directory”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.
| Path | Contents | Clearable |
|---|---|---|
settings.json | Editor, pricing source, preferences | no |
graphs/ | Your workflows and sub-workflows, one JSON each | no |
nodes/ | Custom nodes | no |
skills/custom, skills/imported | Skillsets you authored or imported | custom: no |
payloads/ | Content-addressed context payloads | yes |
search.db | SQLite full-text index over transcripts and payloads | yes |
runs/jobs.jsonl, runs/logs/ | Run history and per-run logs | yes |
runs/injects.jsonl | Lineage — which context went where | yes |
models-pricing.json | Cached model price list | yes |
tmp/ | Scratch files written while injecting context | yes |
The only files threadle writes outside that directory are the ones an agent CLI creates in your project when you run it there.
Network posture
Section titled “Network posture”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: nosniffX-Frame-Options: DENYReferrer-Policy: no-referrerCross-Origin-Resource-Policy: same-originframe-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.
What leaves the machine
Section titled “What leaves the machine”Two things, both of them yours:
- The agent CLIs you invoke.
claude,opencode, and Cursor’sagenttalk to their own backends under your own credentials, as they always do. - 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.jsonis 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-node sandboxing
Section titled “Custom-node sandboxing”Custom nodes are code you placed on disk, so running them is the same trust level as your shell. threadle still fences them in:
| Guardrail | Detail |
|---|---|
| No shell | command is an argv array. There is no shell, so no interpolation, globbing, or injection through node input. |
| Working directory | Pinned to the node’s own folder. |
| Isolated process | Every run is a fresh child process — a crashing or hanging node cannot take the server with it. |
| Timeout | timeoutMs, default 60 000 ms, hard-capped at 300 000 ms. |
| Output cap | 4 MB of process output; beyond that the run fails. |
| Concurrency | At most 8 custom-node processes at once. |
| Environment allowlist | Only 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-out | A 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 too | For 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 nothing | Cloning, 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
Related
Section titled “Related”- Providers — how each adapter reads and runs
- Custom nodes — the extension surface
- Sharing & importing nodes — installing someone else’s node