Please use GitHub's private vulnerability reporting: the Report a vulnerability button on this repo's Security tab. Don't open a public issue, and keep tokens, keys and private thread content out of anything you send.
Threadbear is one person's project, so there's no formal timeline. I'll aim to reply within a few days and to fix anything real on main as soon as I can. Only the latest main is supported.
Threadbear runs Claude Code and Codex agents on your own Mac, with your own logins. Everything is built for one owner per deployment: you, your Macs, your phone and your browser, and a Convex backend you deploy yourself. There are no other users or tenants inside a deployment.
The agents themselves have whatever access you give them. New threads start with full access by default, the same as running the CLIs with permissions skipped. Threadbear doesn't sandbox an agent. Its job is to not hand anything more to code or text you haven't chosen to trust.
- Your Mac from a repo you open. A project's patches (
.threadbear/patches), the scripts they run and its own Claude Code hooks, commands (its agents' and skills' too) and environment settings don't run until you trust them, and in a thread that asks before tools run, nor do its permission rules that would let tools run without asking. Trust covers the exact bytes, including any packages the repo commits in its ownnode_modulesand any workspace package it links there (but not ones you installed), so a later change asks again, and anything it can't check fails closed (protocol: Trusting a folder's patches). A patch's action must be one trust can follow all the way, and the patch is hashed again as the action starts, so a chip never runs bytes that changed since you trusted them. Commands that run by themselves (a patch's live state and list panels, a repo's Claude Code hooks) are held to a short list of shell, and anything outside it doesn't run, rather than being searched for known tricks: a live command may run only a named list of programs in named forms, with literal words, write no file, and use git only with a repo's own config unable to add what git runs (protocol: The short list of shell). What a repo's patch runs on another machine oversshis held to a list too. What the list can't reach: a program that makes its own session outlives the command's process group, and a script outside the repo is checked for who could have written it, not for what it imports. - A schedule's check after you approved it. A check runs by itself on every run, so it's bound to the hash of the command and every file it reaches, kept on the Mac it runs on. If that changes, or it reached the Mac only through the backend, it doesn't run: the thread is told once, and passing the same check again approves it as it is then. Installed packages and programs on PATH aren't covered, and an owner's or Full thread's check that can't be followed all the way (
python3 x.py) is bound only as far as it can be; from a thread that asks first, one like that is refused. - Ask and Auto threads from raising their own access. An agent in a thread set to Ask or Auto can't switch its own or any other thread or schedule to more access, start a thread with more access than its own, send a message to a thread that has more (or move it, or put words in a project brief it reads), or add a schedule check (a shell command), unless you allow that one call on a card. A reply button presses only from its own thread's transcript or message box, so one copied into another thread's reply or ask presses nothing. That card has no "Always allow", and no rule or Auto's classifier answers it. It shows all of what the call does (the whole message or brief, and the folder as the disk has it, links followed), and the call does exactly that or nothing. A thread with full access can already run anything as you, so it isn't asked. The same limits hold for
tbin a thread: each thread's CLI gets a key of its own instead of the runner's token, and anything it asks the runner for above its mode (a thread or schedule above it, a message to a thread above it, raising a mode, answering a card, pressing a button, running an action, trusting a patch, setting instructions, a restart) is refused rather than carded. The runner also checks who's really calling: a connection that comes from under a thread's CLI gets that thread's limits whatever it authenticated with, so reading the token file or wiping the thread's key from the environment gains nothing. A thread that isn't on Full access has Claude's file and shell tools denied the runner'ssecretsfolder as well. What this can't stop is a shell command that deliberately starts a new session outside its thread's process tree and reads the token (Codex's sandbox can read anywhere, too): that's the same-user boundary below, and in Auto it rests on the classifier. A thread started before this release keeps its oldtbuntil its CLI next starts. Claude's "Always allow" on atbcommand covers exactly that command, never a prefix of it. - A sandboxed thread from leaving its sandbox (an experiment, protocol: Sandboxed threads). Its agent counts as restricted even on Full access: anything that would run outside the sandbox asks the same way, and a thread it starts in a sandbox folder stays shut in one. Its login's config folder is shared with that login's other threads, so give sandboxed threads a login of their own.
- Plan first, Ask and Auto on another Mac. A new thread started on another runner keeps the mode you picked through every hop, or isn't started. One an extra Mac's agent asks the main Mac for comes as Ask rather than Plan first.
- "Always allow" on a Codex command covers that exact command in that folder, never a prefix of it, and is only offered for a program whose words say what it does (
ls,grep,rmand the like), never for a shell, an interpreter or a package or build launcher (npm test,make,cargo run), whose code the repo can change. Codex asking for more filesystem or network access is allowed for one turn at a time. - Approval cards show the whole command. Allow never runs more than the card showed: a command or a tool's arguments too long to show whole are refused, and a card the phone can't show in full can only be allowed on the Mac.
- Ask and Auto from becoming code that runs. In those modes an agent's edit to a patch, a file a patch uses or Claude Code's settings always asks, with no Always allow, and what it writes waits for you to trust in Patches. Only a Full access thread's own edits are trusted as it leaves them, and an edit to a file you hadn't trusted doesn't trust the rest of it.
- One Mac from another. Each extra Mac has its own key, which can only act as that Mac. It can't read or change another Mac's threads, commands or secrets, or pair phones. What it sends the Mac and iPhone apps (times, sizes, counts, its machine's figures, the text of its threads) is checked before they use it, so a bad value is left out rather than stopping or freezing the app. It can't get one of its schedules run on another Mac, even under the id of one just deleted there. Its usage readings reach no other Mac, so it can't stop one using a login, and which logins it gets the main Mac's readings of is the main Mac's word (
tb runner logins), never what it says it's signed in to. What it can store is capped in size and number and runs out, and it can't stop the backend's cleanup. Its request for a thread on another Mac shows on the card as that Mac's runner read it from the backend, with nothing it sends stacked over it, and Start it starts only what the card showed. The main Mac's key can do everything (protocol: Runner keys). - Secrets you type on your phone. They're sealed to the Mac named in your pairing code, so the backend and other Macs can't read them.
- Your phone and browser logins. Tokens are stored only as hashes, expire, and can be revoked, which also stops that phone's notifications at once, however many it registered. A token's photo uploads are its own: capped, deleted a day after if never sent, and no token can delete a file it didn't upload.
- Your keys on the wire. The runner sends its key only to an
httpssync URL or Connectors gateway (plainhttponly to the same machine), and never follows a redirect with it. - Shared threads. A share link is a public, read-only bearer link. It shows only that thread, and only pictures from the thread's own media folder. Stopping sharing or deleting the thread takes it down: the runner checks the backend's shared threads against its own on every start and every hour, so a stop or a delete the backend missed doesn't leave the link readable, and none of a thread's new messages go up while the backend's copy is still shared by a link the thread no longer has. The website and share pages look up a reply's links into its own steps with a fixed amount of work per page, so text an agent was steered into writing can't freeze them.
- Your copy from a hijacked upstream. On a fork, only
release/*tags with an SSH signature by the release key setup pinned count as Mike's releases; a PGP or X.509 signature never does. A tag counts only if its commit is on upstream's main, so a key used to sign a side branch gets nowhere. The key can be replaced through a signed release (a new key counts a week after a release signed with a pinned key names it) and revoked (only by itself or a newer key; a revoked key is refused from then on, by the runner and by setup). A release keeps the tag it verified with, and bringing it in merges the commit that was checked. The thread that brings one in works the merge out without checking it out, and runs nothing from the release (no checks, installs or builds) until you say go. Upstream's tags are fetched a bounded few at a time, with a deadline, into a folder outside your checkout, and only one that verifies comes in (protocol: Mike's releases). - Approvals you chose, through a review. A review by another lab (an experiment) runs with the reviewed thread's own permissions, never more, and is told the change and replies it's handed are data, not instructions.
- Another Mac's address. The Mac app only sends a runner's token to the host you pinned for it, never one the backend suggests.
- Your Mac and phone from links and files. A link in a reply, a page, a patch or anything another Mac's runner sends opens without asking only if it's a web page (http or https); any other kind (
slack:,x-apple.systempreferences:) starts its app only after you say yes to that link, by name. A file opens in its app only after its symlinks are followed, as the real file, and without asking only if it's a known kind of document (a picture, text or code, a PDF, a film, an office file) going to an app that shows it: anything else (a.command, a.terminal, an app, a script set to open in Terminal, a kind it can't place) asks first, and another Mac's copy of one never opens. A thread from another Mac never names a file on this one: dragging, Quick Look, a link's hover card and pictures in its replies use only this Mac's copy of that Mac's file. Convex's sign-in opens only Convex's own sign-in page, a review's Open live only a web page, a pin an agent shows only ever opens in the pane (or a web page where you've set links to go), and another Mac's thread never offers a sign-in that runs here. A skill's description in the slash palette keeps its formatting but none of its links. A page's alert, confirm or prompt in the Browser panel is drawn over that page under its site's name, never as one of Threadbear's own dialogs.
- Your Mac account is yours. Other processes running as you are trusted.
- Commits on the
mainyour runner follows are yours. The runner builds, deploys and restarts from it. - Your Convex deploy keys, CLI login and the main runner key stay on your main Mac.
- Remote runners are reached over Tailscale or a similar private network.
- Anyone with a share link can read that thread while sharing is on.
- The push relay is run by me so the App Store app can get notifications. It sees device tokens and encrypted notification text, and only registers Convex deployments that prove who they are. Forks can run their own (
relay/). - Sentry gets crash reports from release builds only, with no thread text.
- What an agent with full access chooses to do on your Mac, including after a prompt injection. That's the agent's permission mode, which you choose.
- Problems that need your Mac account, your Convex admin access or your
mainbranch already in someone else's hands. - The marketing site in
site/, beyond the usual.