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 custom-node folder, or a format change upstream. Not a remote attacker on the open internet, because nothing binds off-loopback.
| Claim | Precise reading |
|---|---|
| Read-only | Discovery parsers never mutate Claude / opencode / Cursor / Antigravity / Codex / Copilot / Grok storage. Runs shell out to those CLIs. they create new sessions in their own storage, exactly as a terminal invoke would. |
| No network from threadle | The threadle process does not upload your code or transcripts, and does not fetch list prices at runtime. Agents you launch still talk to Anthropic, OpenAI, Cursor, Google, xAI, etc. Under your credentials. |
| Localhost only | Bound to 127.0.0.1. Every request still faces Host and Origin guards (DNS rebinding / CSRF). There is no session auth token, because the intended user is you on this machine. |
Read-only against agent storage
Section titled “Read-only against agent storage”Session discovery is parse-only. Files and databases are never modified by threadle.
| Provider | Storage threadle reads |
|---|---|
| Claude Code | ~/.claude/projects. JSONL transcripts |
| opencode | ~/.local/share/opencode/opencode.db. SQLite |
| Cursor | ~/.cursor/projects. Transcripts and chat metadata |
| Antigravity | ~/.gemini/antigravity-cli. Brain JSONL + conversation summaries |
| Codex | ~/.codex/sessions. Rollout JSONL |
| GitHub Copilot | ~/.copilot/session-store.db + session-state/ |
| Grok Build | ~/.grok/sessions. Per-cwd session dirs |
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, billing, preferences | no |
graphs/ | Your workflows and sub-workflows, one JSON each | no |
graphs/versions/<id>/ | Snapshots on save (cap 30). Graph history | |
nodes/ | Custom nodes | no |
skills/custom, skills/imported | Skillsets you authored or imported | custom: no |
favorites.json | Jump-list bookmarks. Favorites | |
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/<jobId>/outputs.json | Per-node text cache for partial / replay runs | yes |
runs/injects.jsonl | Lineage. Which context went where | yes |
triggers.json | Cron / path-watch workflow triggers. Triggers | |
models-pricing.json | Legacy price-list leftover (optional. Prices ship bundled) | 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 / localhost threat model
Section titled “Network posture / localhost threat model”A process on 127.0.0.1:4570 that can launch coding agents with tool access is an attractive target for DNS rebinding and browser CSRF from another tab. Binding loopback alone is not enough.
The server binds 127.0.0.1 on port 4570 (--port to change it). There is no bind-to-0.0.0.0 flag and no remote mode. There is also no auth token. Protection is Host + Origin checking, not a shared secret.
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”One class of hop, and it is yours, not “nothing ever leaves”:
- The agent CLIs you invoke. Pressing ▶ sends prompts and project context to Anthropic, OpenAI, Cursor, or whoever that CLI is configured for, exactly as running the CLI yourself would. threadle is the local driver. The network hop is the agent’s.
List prices do not leave or enter at runtime. They ship as a vetted snapshot with the package (CI refreshes from models.dev via PR). See Costs.
No telemetry, no analytics, no update check, no crash reporting from threadle itself.
Security view
Section titled “Security view”Security in the sidebar lists:
- Elevated permission modes on discovered sessions (Claude
meta.permissionMode≠default). - Workflow agent settings. Agent-def nodes across saved graphs with non-default Claude
permissionModeor Codexsandbox/askForApproval(scanned from loaded graphs). - Live sessions and the static safeguards list (localhost, Host/Origin guards, read-only discovery, argv-only injects).
Per-agent posture is also set on the agent card / inspector. See Agents.
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 a fixed allowlist (PATH, HOME, USERPROFILE, TEMP/TMP, …) plus THREADLE_NODE and the node’s own params. The server’s environment. API keys included. Is not inherited by default. |
| 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.
| Surface | Trust notes |
|---|---|
threadle mcp | Hosts spawn the process. Published workflows run with approveAll: false (gated graphs stay in the UI). Default publish allowlist is empty (none). List graph ids or set null / * for all. Nested MCP recursion is refused. |
| ◈ MCP tool node | Calls a discovered server: stdio (command you declared) or HTTP/SSE (url + optional headers). Same class of risk as a custom node for stdio. Remotes use whatever auth you put in headers. Child env for stdio is the custom-node allowlist, plus that server’s own env. Not a full copy of the threadle process. Turn the client off or disable individual servers in Settings. |
Unix-domain sockets / WebSockets are not supported.
See MCP.
- Never mutates
~/.claude/projects(runs may create new Claude sessions via theclaudeCLI) - Never mutates the opencode SQLite database (same: new sessions via
opencode) - Never mutates Cursor transcript files (same: new sessions via
agent) - Never mutates Antigravity brain / conversation stores (same: new sessions via
agy) - Never mutates
~/.codex/sessions(same: new sessions viacodex exec) - Never installs, updates, or executes a custom node on its own
- Never uploads your transcripts, prompts, or outputs from the threadle process itself
Related
Section titled “Related”- Providers. How each adapter reads and runs
- Custom nodes. The extension surface
- MCP. Workflows as tools / canvas client
- Files & config. What lives under
~/.config/threadle - Sharing & importing nodes. Installing someone else’s node