ShellPilot handles SSH keys, passwords and database credentials. Security reports are taken seriously and are welcome.
For the AI & MCP bridge's threat model specifically — what an AI agent can and cannot reach, and where that design's limits are — see docs/AI-SECURITY.md.
Please do not open a public issue for a security problem.
Use GitHub's private vulnerability reporting, or email the maintainer at aliwaqarofficial@gmail.com. Include:
- what the issue is and roughly how severe you think it is
- steps to reproduce, or a proof of concept
- affected version and platform
You can expect an acknowledgement within a few days. We will keep you updated while a fix is prepared, and credit you in the release notes unless you would rather stay anonymous.
Fixes land on the latest release. There is no long-term support branch yet.
Knowing the design may help you assess a finding.
| Data | Storage | Protection |
|---|---|---|
| SSH passwords, key paths, key passphrases | shellpilot-secrets.json |
Electron safeStorage — DPAPI / Keychain / libsecret. Machine and user bound. |
| Vault entries | shellpilot-vault.json |
AES-256-GCM, key from the master password via scrypt (N=32768). Password never stored. Decrypted entries are sent to the renderer while the vault is open, so they live in the renderer's memory too — not only in the main process. |
| Biometric unlock key (opt-in, off by default) | main-process memory, or shellpilot-vault-bio.json |
The vault's derived key, wrapped with Electron safeStorage. By default this is held in memory only and dies with the process, so nothing is written to disk. The file exists only if you explicitly choose to keep biometric unlock across restarts. |
| Workspace passwords | shellpilot-wslocks.json |
scrypt verifier with a random salt, compared with timingSafeEqual. Not reversible. |
| Trusted SSH host keys | shellpilot-known-hosts.json |
SHA-256 fingerprints, plaintext (not secret). |
| Backups | user-chosen .spbackup file |
AES-256-GCM under a passphrase you supply. Credentials are unsealed from the keychain and re-encrypted so the file is portable. |
| Servers, folders, workspaces | shellpilot-data.json |
Plaintext. Contains no credentials. |
| AI/MCP agent sessions | shellpilot-mcp-sessions.json |
Only a SHA-256 hash and a 4-character preview of the bearer token is stored — never the raw token. |
| AI/MCP audit log | shellpilot-ai-audit.jsonl |
Plaintext, append-only. Free-text fields are passed through the same secret-redaction as command output before being written, so it should never contain a credential. |
| Local terminal sessions | shellpilot-local-sessions.jsonl |
Plaintext, append-only, 0600. One entry when a local shell starts and one when it exits — shell label, resolved path, pid, working directory, exit status. Never keystrokes and never output. Kept separate from the AI audit log, which answers a different question. |
| AI/MCP access-group policy | shellpilot-ai-policy.json |
Plaintext. Contains no credentials — capability rules and file-path patterns only. |
ShellPilot does trust-on-first-use host key checking against its own
shellpilot-known-hosts.json. Without it, ssh2 accepts any key presented, so
anything on the path to a server could capture the session and the credentials
sent over it.
It also reads ~/.ssh/known_hosts (and known_hosts2), but never inherits
trust from it. Adopting that file wholesale would silently take on every trust
decision it has accumulated, including whatever StrictHostKeyChecking=accept-new
waved through unattended. So an entry there only changes what the prompt says:
a host whose exact key OpenSSH already has is presented as recognised, and the
dialog defaults to Trust instead of Cancel. A human still confirms it once.
Two cases there are not merely informational:
@revokedmatching the presented key refuses the connection outright. It is a negative signal, so acting on it without asking only ever restricts.- A host present under a different key is called out explicitly in the prompt, since that is either a rebuilt server or an interception.
@cert-authority lines are ignored. They authorise certificates rather than
naming a host key, and ShellPilot cannot validate a certificate chain — so they
are treated as no evidence rather than as trust.
Once a host is trusted, a changed key is always refused outright and never re-prompted: the user has to forget the saved key in Settings → Security first.
macOS builds run with the hardened runtime, which blocks
DYLD_INSERT_LIBRARIES injection and debugger attach. This matters more than
any of the vault's own protections: without it, a process running as the same
user can inject into ShellPilot and read an unlocked vault key out of memory,
and no amount of encryption at rest or biometric gating prevents that.
The hardened runtime does not require an Apple Developer certificate — it works with the ad-hoc signature these builds already carry.
Library validation is disabled by entitlement, because asarUnpack keeps the
ssh2 and cpu-features native binaries outside the archive deliberately. That
gives back one of the three protections the hardened runtime provides and keeps
the two that defend an unlocked vault.
Workspaces isolate servers, databases and tunnels — each record carries a workspace id and is filtered by it. A password-protected workspace keeps those out of sight until it is unlocked.
The vault is filtered by workspace, not separated by it. A vault entry can belong to a workspace — new entries belong to the one they were created in — and the entry list then shows that workspace's entries plus any marked shared. Entries written before this existed have no workspace recorded, so they stay shared and can be moved deliberately.
What that is worth, stated precisely: it stops another client's credentials being in front of you, and it stops you saving a credential into the wrong place by accident. It is not a cryptographic boundary. The vault remains one encrypted file under one master password, so anything that can read the unlocked vault can read every entry in it regardless of workspace. Locking a workspace does not lock the vault.
Per-workspace cryptographic separation would mean a master password per workspace, which is a different product; this is a view over one vault.
Worth stating plainly, because it is easy to assume more.
Electron's promptTouchID authenticates the person at the keyboard. It does not
return a key. So enabling Touch ID unlock stores the vault's derived key on disk
under safeStorage, with a biometric prompt in front of reading it — a gate,
not a cryptographic binding. Software running under your macOS account can read
that key without ever triggering the prompt.
The stronger designs — a Keychain item with a biometry ACL, or a Secure Enclave key wrapping the vault key — both require the macOS data-protection keychain, which requires entitlements authorised by a provisioning profile, which requires a paid Apple Developer account. ShellPilot is ad-hoc signed and has none, so neither is available to it. (Apple's TN3137 documents this; it is the design, not a gap we have failed to close.)
This is why biometric unlock is session-scoped by default: the wrapped key is held in main-process memory and dies with the app, so an attacker who can read your files finds nothing to read. You type the master password once per launch and Touch ID reopens the vault after that. It is the same design KeePassXC uses, and it is what makes the feature defensible without the entitlements above.
Keeping it across restarts is a separate, explicit choice, and it is the one that writes the key to disk.
Two consequences worth being concrete about:
- With biometric unlock off, the master password exists nowhere on the machine, and the vault is the only ShellPilot data an attacker with your files and your logged-in session cannot read. Turning it on gives that up.
- Your SSH credentials in
shellpilot-secrets.jsonhave always been protected bysafeStoragealone. So enabling this moves the vault down to the protection the rest of the app already has, rather than opening a new category of risk.
It remains a real barrier against someone who picks up an unlocked laptop, and it is why the feature exists. It is not a barrier against code running as you.
ShellPilot can open a shell on the machine it is running on — your own zsh or bash, PowerShell, Git Bash, a WSL distribution — in a tab next to the SSH ones. Two things about that are worth stating, and they pull in opposite directions.
It is not a new privilege. The shell runs as you, in your environment, with whatever your account can already reach. Terminal.app, Windows Terminal and your desktop's own launcher have always been one click away, and nothing here hands a person at your keyboard anything they did not already have. Read no more into it than that.
What changes is the distance. Two statements above rest on an assumption this feature does not break but does bring within reach: Process hardening says the hardened runtime is what stops a process running as the same user reading an unlocked vault key out of memory, and What biometric unlock actually protects says it "is not a barrier against code running as you." After this feature, code running as you is a UI affordance inside the same window, while the vault is open. Concretely, a shell started from that tab can read — with no prompt, no elevation and no ShellPilot involvement:
shellpilot-vault-bio.json, if you chose to keep biometric unlock across restarts. It holds the vault's derived key, andsafeStorageunwraps it for anything running as you. (This is the same point the section above makes; it is repeated here because the terminal is where it becomes convenient.)shellpilot-secrets.json— SSH passwords, key paths and key passphrases. On Windows (DPAPI) and Linux (libsecret)safeStorageis scoped to the user, not to the application, so any process running as you decrypts it.shellpilot-ai-policy.json— the access-group rules that constrain an AI agent.shellpilot-mcp-sessions.json— which agent sessions exist and when they expire.shellpilot-ai-audit.jsonl— the record of what an agent did. Append-only to ShellPilot; an ordinary writable file to a shell.
That list is exactly why the local terminal is a human-UI-only surface. It is
not exposed over the MCP bridge or the shellpilot CLI, it is not behind an AI
capability, and it is not behind an ASK prompt — because no value of either would
make it safe. Every constraint ShellPilot advertises to an agent is a file on the
same disk as the shell, so an agent that can run one local command can read the
policy store that limits it and the audit log that records it. The answer is "not
reachable", not "gated".
That is enforced by tests/localTerminalNotExposed.test.ts rather than by
reviewer memory. It asserts the tool list the bridge actually serves against a
reviewed whitelist, walks the transitive import closure of
src/main/services/mcpServer.ts and everything under src/cli/ to prove neither
can so much as import the pty module, and checks that no AI capability names a
local shell. If one of those fails, the failure is the finding.
The local:* IPC handlers are gated in the main process
(src/main/services/localGate.ts), not only in the renderer: the
localTerminalEnabled setting is mirrored on the main side and the connect
arguments — session id, working directory, terminal dimensions — are validated
there, because a renderer-side flag constrains only an honest renderer. Starting
ShellPilot with ELECTRON_DISABLE_LOCAL_TERMINAL=1 stops the pseudo-terminal
binding being loaded at all. Neither is a security boundary against someone at
your keyboard — they have a terminal either way — they are there so a machine can
run ShellPilot without the feature.
These are design decisions, not bugs. Please do not report them as vulnerabilities — but do open a discussion if you disagree with the tradeoff.
- Workspace passwords gate the UI only. They do not encrypt that workspace's servers on disk.
- A backup passphrase cannot be recovered. There is no escrow and no backdoor.
safeStorageis machine bound. Copying the config folder to another machine will not carry credentials — use an encrypted backup instead.- Releases are not code-signed. Verify checksums if you need assurance about a download.