Shellby gives an AI agent hands on your PC,
so it's built to keep those hands where you can see them.
Report a vulnerability Β· At a glance Β· Know the limits Β· Privacy policy
Important
Found a security problem? Please don't open a public issue. Use GitHub's private reporting: Report a vulnerability β. You'll get a reply within a week.
| Promise | In short | |
|---|---|---|
| π | Shellby never answers for you | In every mode except Autonomous, Claude Code enforces the permissions and waits for your answer. Shellby only relays it. |
| πͺ | Going further needs a separate window | Anything that would mean fewer prompts, or answers from somewhere else, goes through an isolated confirmation window that even a compromised panel can't click. |
| π΄ | Autonomous is never the default | It needs the confirmation window the first time, asks again once each time Shellby starts, and shows in red. |
| π¦ | Every window is sandboxed | A strict CSP with no remote content and no inline scripts. Each window gets a fixed list of IPC calls, and the main process validates them and checks which window is calling. |
| π§Ύ | Untrusted text is escaped or cleaned | Claude's replies are HTML-escaped, friends' crabs and plugin names are cleaned, and workflow values never become code. |
| π | Local data, no telemetry | History transcripts live in %APPDATA%\Shellby\sessions. The online features you can turn on are listed in the privacy policy. |
Contents: Permissions Β· The app itself Β· Unattended work Β· Your credentials Β· Online features Β· Things that touch your PC Β· Plugin listener
Claude Code enforces permissions; Shellby only relays your answers. In every mode except Autonomous, any action Claude Code would prompt for is sent to Shellby as a can_use_tool request and blocks until you answer. Shellby never answers on its own. Pending requests are denied if you stop the task or the panel session ends.
-
The panel answers; the confirmation window guards the rest. Permission cards are answered in the panel, so the panel can allow what Claude asks for. Anything that would go further (fewer prompts, or answers from somewhere else) goes through a separate, isolated confirmation window instead. That window has:
- its own sandboxed process and bridge
- answers accepted only from that window
- Cancel as the default
- one at a time: a stack of them is refused, not queued
The panel, even a compromised one, cannot click it.
-
Autonomous mode (
bypassPermissions) is never the default. Turning it on the first time needs the confirmation window, and switching into it asks again once each time Shellby starts. The UI shows the mode in red. -
!commands (a message starting with!runs in PowerShell, as you) are confirmed in the window the first time. After that they are yours to use, like a terminal. PowerShell is started by its full path in System32, never by a name a project folder could shadow. -
Self-built tooling is flagged. Shellby tracks the files Claude writes in each conversation. A permission card warns when a command runs one of them, and when a write or command touches Claude Code's own setup (skills, agents, commands, hooks, settings,
CLAUDE.md, MCP config). -
Subagent prompts go through the same gate. Permission requests from helper agents use the same
can_use_toolchannel and are labelled with the helper that asked.
- Renderers are sandboxed. Every window runs with
sandbox: true,contextIsolation: trueandnodeIntegration: false, behind a strict CSP (default-src 'none', no inline scripts, no remote content).- Navigation, new windows and browser permissions (camera, microphone and the rest) are refused.
- Each window's preload exposes a fixed list of IPC calls, the main process validates their arguments, and every handler checks which window is calling.
- The crab's window, which draws friends' visiting crabs and stickers, has a bridge of its own that can only move him and take a file drop.
- Paths a renderer hands over are never opened on a network share, so nothing makes Windows sign in to another machine.
- Model output is untrusted. Claude's replies are rendered by a small Markdown renderer that HTML-escapes everything first and only emits a fixed set of tags. Links are inert until clicked, only
https:links open (in your browser), and hovering one shows where it really goes, whatever its text says. File paths become links too, and open only when clicked, only if the file exists: in your editor through its ownvscode://-style link (nothing goes through a shell), or else in Windows' default app β except anything that would run, mount or install rather than open (scripts, executables, installers, disk images, macro documents), which is only shown in its folder. - Skins are data, not code. They're JSON, validated against a strict schema (hex colours only, bounded size), and drawn with DOM APIs.
- Local data only. History transcripts live in
%APPDATA%\Shellby\sessions, with tool inputs (and the text of a requested edit) stripped from saved permission requests. The text of Claude's file edits is kept with the step that made them (capped in size), so their diffs show again when you reopen a conversation; Claude Code's own transcript in~/.claudealready holds the same. There's no telemetry.
Workflows run unattended, with every step's permission mode fixed when you save it.
Note
Saving a workflow that can act without asking shows the confirmation window. That covers a command, a web request, a file write, an MCP tool step, MCP servers a Claude step may use unasked (in any mode), a Claude step in Smart, Auto-edit or Autonomous, running another workflow, opening a pull request, and starting or stopping a dev server. The window lists every such command, prompt, address, header name, body and file in full. It asks again whenever any of them, its folder or its triggers change.
A workflow Claude proposes is always confirmed and can't use Autonomous. If it's too long to show in full, it's refused outright rather than shown shortened.
Outside data stays data. Values from a trigger, a web response, a file or an earlier step never become code:
| Step | How outside values get in |
|---|---|
run |
Each {{ value }} is passed to PowerShell as an environment variable (${env:SHELLBY_VALUE_1}), never in the command's text. |
| Claude | Values arrive inside «», with a line saying they're data, not instructions. |
| Web request | Values are encoded after the base address, and a value that changed the host is refused. |
| File path | A value that would leave its folder is refused. |
Secrets (Automate β Secrets):
- are encrypted by Windows and never sent back to the panel
- are only allowed in commands and web requests, never in prompts, messages or files
- reach commands through the environment rather than the command line
- are blanked out of everything a run records
Redirects to another site drop a request's headers, and https never redirects down to http.
Limits:
| Runs at once | Steps per run | Items per loop | Workflow calls deep | Paused after |
|---|---|---|---|---|
| 4 | 1000 | 100 | 3 | a workflow starts more than 60 times in an hour |
A folder trigger watches only the top level of its folder.
Routines run unattended with their own saved permission mode. Saving one that doesn't ask first (Smart, Auto-edit or Autonomous) is confirmed in the window whenever its prompt, folder or mode changes. Autonomous routines are only possible after you've turned Autonomous on in the confirmation window.
- Claude's sign-in is Claude Code's. Shellby never sees your Claude credentials. Claude Code gets the environment as it is on your PC, API keys and provider switches included. Always use my Claude plan (Settings β Claude) leaves out
ANTHROPIC_API_KEY,ANTHROPIC_AUTH_TOKEN,ANTHROPIC_BASE_URLand the Bedrock, Vertex and Foundry switches, so it always runs on your plan. - Your GitHub token, if you let Claude use it. Shellby keeps its GitHub sign-in encrypted by Windows. If you turn on Let Claude tasks push code and open pull requests (Settings β Connections β GitHub), that token goes into Claude Code's environment as
GH_TOKENandGITHUB_PERSONAL_ACCESS_TOKEN, sogh,gitand GitHub MCP servers work.
Warning
With Let Claude tasks push on, anything Claude runs can read your GitHub token. It is off unless you turn it on.
Notifications go only where you confirmed.
- A new destination (provider, topic or chat, token), or turning on Allow/Deny answers from the phone, is confirmed in the confirmation window. Nothing is sent to a destination you haven't confirmed.
- A random ntfy topic Shellby makes up for you, with answers off, needs no question.
- Answers from the phone use single-use codes, and prompts the card would warn about can only be answered at the desk.
- Plain
http:is only used for this PC and private network addresses.
Off by default, Telegram and ntfy only, and turned on only through the confirmation window, which says what it allows. The yes is bound to the provider, chat or topic and token (hashed), so a different bot, chat or topic turns it off until you confirm again. A folder for the tasks is picked in the system folder picker first; the panel can't name one.
- What counts: on Telegram, only a new text message typed in your own private chat with the bot (chat and sender both your confirmed chat id, chat type private): not edits, forwards, channel posts, bots, groups, or messages from before you turned it on or more than 10 minutes old. On ntfy, only posts to
<topic>-tasksthat start with a passphrase Shellby generated (about 79 bits, kept encrypted by Windows, compared in constant time); a post without it is ignored silently. Both need a destination you already confirmed, and an ntfy topic nobody will guess or an access token. ntfy's read position is saved, and message ids are remembered, so nothing is replayed after a restart. - What it can do: the text only ever becomes the words of a prompt. Lines starting with
!and a leading/are refused, snippets are never expanded, and nothing from the phone can change the permission mode, the model or any setting. Text is capped at 4000 characters and stripped of control, bidi and zero-width characters. Every phone task runs in Ask first, whatever mode the desk uses, in its own copy when the folder is a git repository, and waits for the reset when the spending guard would hold it. At most 10 start an hour and 3 run at once, and messages past 60 an hour are dropped unread./statusanswers with tab titles and states only. - Threat model: someone who steals your bot token, or learns your ntfy tasks topic and passphrase (anyone who can read that topic sees the passphrase in your posts, so keep the topic to yourself, or use an ntfy server that wants a token), can ask Claude to do things in your projects. They still can't make it act: every edit and command needs an Allow, either at the desk or from a phone button tied to a single-use code (which they could press too if they hold the same bot token or can read the reply topic, so treat the token and topic like a password, and Let me answer Allow or Deny from my phone off is the stricter setting). Reading files doesn't prompt in Ask first, so a task can read your projects; what it found shows at the desk, while Shellby's own answers to the phone carry only titles and states. (A phone task held for the reset reports its result like any queued task, if you have those notifications on.) Polling stops when Shellby quits.
Off by default, and turned on only through the confirmation window, which names the Tailscale login it's for and says what a paired phone can do. How to set it up is in docs/PHONE.md.
- Where it listens:
127.0.0.1:47916, never another interface, so nothing on your network reaches it directly. Shellby asks Tailscale'sserveto put it athttps://<this-pc>.<tailnet>.ts.neton your tailnet, with a certificate Tailscale manages, and asks Tailscale to stop when you turn it off. It never turns on Funnel, and it reads Tailscale's serve settings when it starts and every minute while it's on: if Funnel (or a raw TCP forward) points at its port, it stops serving and says why in Settings. - Who gets in: every request must have a
Hostof127.0.0.1orlocalhoston its port, or this PC's own.ts.netname; anything else is refused as DNS rebinding. A request that came through Tailscale (its.ts.netHost, or anyX-Forwarded-*,Forwarded,ViaorTailscale-*header, whatever Host it claims) must carryTailscale-User-Login(which Tailscale sets itself and strips from what the phone sends) equal to the login you turned it on with, so a tagged device, which gets no login, is refused too; one markedTailscale-Funnel-Requestis always refused. AnOrigin, when a browser sends one, must be the server's own, and it never answers a CORS preflight, so another web page in your phone's browser can't call it. - Pairing: a phone gets in with a 32-byte token it keeps in its own storage and sends in a header (never a URL). It earns one by typing an eight-character code shown only in Settings, which lasts five minutes, takes five tries in all and works once. Pairing attempts are capped at 30 an hour. Shellby keeps a SHA-256 hash of each token, compared in constant time, never the token. Forget in Settings ends that phone's streams at once, and a different Tailscale account turning it on forgets every phone.
- Each phone's rights: a level on its record, set only in Settings (never from the phone): Full, or Read only. A read-only phone's calls are checked against one allowlist on the PC before anything else runs (looking at the lists past the beach and a conversation's files, its own notification subscription, a pat, and forgetting itself); every other action, and any photo upload, is refused with a plain message. The phone app hides the buttons too, but the server never relies on that. The level is read from the record as it is now, so a change at the desk reaches a phone mid-stream. Records from before the level existed count as full, which is what they had.
- A tap on a notification carries only a path inside the app (
/#/prompt/<conversation>/<request>, both ids checked against[\w-]{1,100}), never a token or a URL elsewhere: the installed app answers with its own device token through the sameansweraction and the same rules as the card on the beach. - Allow and Deny on a notification (Android shows them): off until you allow it in Settings, since Android can run a notification's buttons from a locked phone. Then the push carries the two ids, never a token, and only for a prompt the beach card could answer with those two buttons (not a question, a plan or anything
appDeskOnlyReasonflags), and only to a phone with full rights. The service worker answers through the sameansweraction, with the device token the app keeps a copy of in its own Cache Storage for it (same origin, same reach as its localStorage, deleted when the phone is unpaired). The PC checks the rights and the prompt again; when it refuses, a notification says why. - Shared from another app (the manifest's
share_target): the service worker keeps the words and up to four pictures on the phone, and the app opens New task with them. Nothing is sent until you press Send, and the pictures go through the same capped upload as any other. - What a phone can do: the same things, through the same code, as the desk. Allow, Deny, and Always allow only where the desk's card offers that rule; questions and plans it shows in full are its to answer (
appDeskOnlyReason), and everything elsedeskOnlyReasonflags (long commands, a file Claude wrote, Claude Code's own setup, a plan too long for a phone) can only be denied there. Each answer is recorded as from the phone. New tasks only in a project Shellby knows, in the mode the phone picks from the desk's list (Ask first unless it picks another, and Autonomous only once it's been turned on at the desk) and on a model from the desk's own list, in their own copy when it can, at most 30 an hour and 10 at once, and held for the reset at the limit; a task that starts with!or/is refused. Look over my changes runs the desk's read-only review prompt, always in Ask first, in a project from the phone's list or a conversation's own folder, under the same limits. Replies go to any open conversation not handed to a terminal, in a mode the same rules allow; a reply can't start with!, and the only/commands it can run are/compact,/contextand/goal. Besides that, the phone can rename a conversation, set its effort and close it; it never changes the model or a setting. Undo and the diff name a conversation and a turn; main finds the turn's snapshot in History, and a file is only read if it's in that turn's own list. Bring it home goes through the desk's checks gate, and forcing it past red checks needs a second yes on the phone. At most 120 calls a minute per phone, and bodies are capped at 16 KB. - The app itself is a fixed table of files built at startup (no part of a request ever becomes a path), served with a strict CSP (
script-src 'self', no inline script, no other host, no framing). Everything from the PC goes on screen as text, never markup; Claude's links open only when they'rehttps:. - Notifications are encrypted on this PC (RFC 8291) and signed with a key only this PC has (VAPID, RFC 8292), kept encrypted by Windows. The push service sees only ciphertext. A subscription the phone hands over is only accepted for Apple's, Google's, Mozilla's or Microsoft's push service, over https on the standard port, so a pairing can't aim Shellby's requests anywhere else on your network.
- Threat model: someone with your Tailscale account and one of your devices, or your unlocked phone with Shellby paired, can see your conversations and ask Claude to do things in your projects. They still can't make it act without an Allow, and what the desk would warn about waits for the desk. Forget the phone, or turn Shellby on your phone off, to end it.
Off by default and never granted by a first sign-in. It's turned on only through the confirmation window, because it publishes a public gist.
- What you publish: the card holds only your crab's look and level, under your GitHub username. Turning it off or signing out deletes the card.
- What you receive is untrusted. Friends' cards and gist comments come from other people, so:
- a card counts only if GitHub says the gist belongs to that friend
- every field is cleaned to known keys and lengths before it reaches a window
- item keys are only looked up in your own catalog, so a card can't carry pixels or markup
- waves are a marker for one of a few fixed lines, and only friends you added are delivered, so nobody can choose what your crab says
A shellby://install link carries only a pack id. Before anything installs, Shellby:
- looks the id up in the official registry index
- downloads only from the registry's own origin and path (redirects are checked too)
- enforces a size cap while streaming
- verifies the SHA-256 checksum from the index
- requires the pack's id to match the link, and validates it
- shows the isolated confirmation window listing every item
Nothing installs without that confirmation. The gallery's PR check always runs the validator from the trusted main branch.
Caution
Plugins are code: they can add hooks and MCP servers that run programs. The shop is treated like a permission prompt.
- Claude Code does the installing. Shellby only runs
claude pluginsubcommands viaexecFilewith an argument array (no shell), from an empty folder of its own so nothing resolves to a local path. It never downloads or writes plugin files itself. - The panel can't choose what runs. Install and Remove accept only a plugin id that the CLI itself just listed. A new marketplace must normalize to a GitHub
owner/repoor a publichttps:URL. Flags, local paths, other schemes, IP addresses and local host names are refused. The dialog shows the normalized value, which is exactly what Claude Code receives. - Nothing installs without the isolated confirmation window. It names where the plugin really comes from. A marketplace counts as Anthropic's only if both its name and its source match, and anything else gets a red warning. Cancel is the default button, and only one shop dialog can be open at a time. Hooks and MCP servers can't be listed before installing (Claude Code only reports them afterwards), so Shellby reports them right after the install.
- No auto-accept. Shellby never passes
-y. A plugin whose marketplace installs it by running a command is refused, with the terminal command to review it yourself. Plugins installed for a single project are removed from a terminal in that project, not from Shellby. - Text and links are cleaned. Names and descriptions are reduced to one line with control, bidi-override and zero-width characters removed. "Source" links open only
https:URLs derived from the catalog, never a URL sent by the panel. Marketplace catalogs are read only from Claude Code's own~/.claude/pluginsfolder.
When a conversation works in its own copy (a git worktree), Bring home commits and merges Claude's work with git hooks turned off. That's because Claude can edit a tracked hook (.husky/pre-commit, say) without a prompt in Auto-edit. Push is yours and runs your hooks as a terminal would.
Line them up (in the Ctrl+Shift+A list) rebases finished copies onto each other with hooks off too. It rewrites branches, so it asks first. It never touches a copy with uncommitted work (stashing would use the stash every worktree shares), aborts a rebase that conflicts so the copy is back where it was, and stops at the first red check.
Off until you turn it on. It reads the clipboard and the last failed background command only when you press it, takes out anything that looks like a secret (the same redaction as dev server logs, then any line the push secret scan still flags is dropped whole), and puts it in a new task's box, fenced and marked as data, not instructions. Nothing is sent until you send it.
Continue in a terminal opens a window of your own. Windows Terminal or PowerShell runs a script passed with -EncodedCommand, in which the folder and Claude Code's path are single-quoted PowerShell literals (every kind of single quote doubled), and the session id must be a UUID. The cmd fallback is only offered when Claude Code's path has none of cmd's special characters, and gets the folder as its working directory, never on its command line. The billing variables are removed inside the window when Always use my Claude plan is on (src/main/handoff.js).
- The per-project security review is a fixed prompt (
src/main/review.js) that tells Claude to report and not to edit, commit, push or fix. - The task runs in Ask-first mode whatever mode you're in, so every tool it reaches for still comes to you.
- Only a folder Shellby already tracks as one of your projects can be reviewed. The folder name is stripped of control characters and clipped before it goes into the prompt, and the prompt tells Claude to treat the code it reads as data rather than instructions.
- It's scoped to your pending changes, and it is deliberately not allowed to tell you that you're secure.
| What | How it's kept safe |
|---|---|
| Processes | Shellby runs only these programs, all with fixed arguments and no shell: nvidia-smi (a query), PowerShell scripts that list drives, processes and startup apps, tasklist, wsl --list and docker system df. Nothing from a renderer or a sensor ends up on a command line, beyond a checked process id. End task asks in the confirmation window first, and switching a startup app off or on works the way Task Manager does, so it can always be switched back. |
| LibreHardwareMonitor | Reached only at http://127.0.0.1:<port>, with the port limited to 1024β65535. Responses are size-capped and parsed as data. |
| Hardware names | Reduced to one line of at most 80 characters before they're shown or put into an Ask Shellby why prompt. |
| Ask Shellby why | Fixed templates that tell Claude not to delete, kill or change anything, run under your normal permission mode. |
| Dev scenarios | Fake sensor scenarios exist only in development builds. |
- Asks first: Shellby edits Claude Code's
~/.claude/settings.jsononly after you confirm in the isolated confirmation window, which shows any status line it would replace. - Backups: it keeps a backup of the file, never writes a settings file it can't parse, and Remove restores your previous status line (unless you've changed it since).
- What gets added: a single
bashcommand that prints a temp file Shellby writes. That file holds only Shellby's mood, level and health, and how things stand: a streak, a usage limit or focus timer counting down, failing CI, commands left running and XP just earned.
While Claude Code everywhere is on, Shellby listens on 127.0.0.1:47913 (never other interfaces) on four routes.
What it accepts. Every request must have:
- β
an
X-Shellby: 1header - β a JSON content type
- β
a
Hostof127.0.0.1orlocalhost - β no
Originheader
Browsers always send Origin on cross-site requests and can't add custom headers without a CORS preflight that Shellby never answers. A page that rebinds its own name to this PC still sends that name as Host. So a web page can't reach it.
What it keeps. Bodies are capped at 2 MB while they arrive. From hooks, only the event name, tool name, folder, session id and the app Claude Code runs in (VS Code, Cursor, a terminal) are kept, clipped to short single lines, plus the program name of a command left running in the background and the first 160 characters of a permission notification (which can go to your phone). The folder and id are what Bring it into Shellby needs to open the conversation. Commands and their output are read for what they mean (tests passing, a push, a dependency checkup, a bug for the Bugdex) and then dropped; file contents are never read.
| Route | Used by | What it can do |
|---|---|---|
/v1/hook |
The plugin's hooks | Events change the crab's mood, count finished turns and XP, note the project for streaks and stickers, and can send "he needs you" and "done" to your phone if you set that up. They can't start tasks, answer permissions or change files. The only git Shellby runs for them reads, with core.fsmonitor off, so a repository's own config can't name a program to run. |
/v1/crab |
The plugin's MCP tools, and shellby say, status, projects and next |
Claude can make the crab say something or put on something you own. With the token Shellby keeps in its own profile folder, it can also propose a routine or a workflow, list your routines and workflows, start a workflow you gave the When Claude Code asks trigger, read your Projects page (your projects, what's next, a dev server's log) and a project's journal, add to or tick off a project's to-do list, and pin a note to its journal. A proposal is only saved after you confirm it in the confirmation window, and Autonomous is never accepted from it. |
/v1/cli |
The shellby command's do, take, flow, time and snippets, if you install it |
shellby do starts a task in any mode except Autonomous, and shellby flow run starts a workflow you gave the When Claude Code asks trigger. shellby take opens an outside session Shellby has already heard from (by its id, or the newest in the folder named) in a tab, and sends nothing to it. It needs a token Shellby keeps in its own profile folder. |
/v1/flow |
Workflow web hooks | Starts the one workflow whose Web hook trigger holds the 48-character token in the body, compared in constant time. No token, no run. The posted data is treated like any other outside data in a run. |
- The plugin's hook script never prints and always exits 0, so it can't influence Claude Code.
- XP: a successful Bash or PowerShell command's text is checked once, in memory, to see whether it ran tests, pushed or deployed. Only that meaning (and the folder name) is kept, never the command.
What these protections don't cover:
- Other programs running as you. Any program running as you on this PC can reach the plugin listener (and the phone app's port, though not without a paired phone's token), as with any local port, and that includes reading the
shellbycommand's token. The listener keeps out browsers and other machines, not your own programs. Another program holding the port before Shellby starts would get what the plugin's hooks send; Shellby shows the port as busy when that happens. - Autonomous mode (
bypassPermissions) skips the permission prompts above. It's never the default and has to be confirmed in the confirmation window. - A GitHub token handed to Claude is readable by whatever Claude runs, if you turn on Let Claude tasks push.
- Plugins you install are code, with whatever hooks and MCP servers they bring.
Please don't open a public issue. Use GitHub's private vulnerability reporting: the Security tab β Report a vulnerability. You'll get a reply within a week.