Husk is a desktop wrapper around a CLI agent. It runs entirely on your machine. This document describes the threat model, what Husk does to protect you, what it does not, and how to report an issue.
Husk's job is to give a CLI AI agent a usable surface. That means:
- It spawns and controls a child process (
claude,copilot,codex,aider,gemini) inside a real PTY. - It reads and writes user data under
~/.claude/,~/.config/husk/, and~/.local/share/husk/. - It downloads optional components on user request: Piper TTS binary and voice models on Linux.
- It writes MCP server entries (including their environment variables and HTTP headers, which may carry API keys or OAuth tokens) into the active agent's own config file:
~/.claude.jsonfor claude,~/.copilot/mcp-config.jsonfor copilot. - It executes user-provided commands when configured. The agent CLI is launched with the per-platform spawn rules described under "Process model" in
docs/architecture.md; user-supplied MCP server commands are stored verbatim in the agent's own config and are run by the agent, not by Husk.
In scope for hardening:
- Local privilege boundary: data Husk writes should not leak to other local users.
- Renderer isolation: a malicious skill description, session title, PTY-injected payload, or clickable URL should not pivot into Node or OS execution.
- Safe IPC surface: every
window.husk.*API validates input and refuses paths outside its intended directory. - Supply chain for fetched scripts: any script Husk's installer runs from a third-party URL is pinned to a known hash.
Out of scope:
- A user account on the machine that has already been taken over by someone else.
- Mitigating malicious agent CLIs the user explicitly configures and runs.
- Network-level threats against the agent or the MCP servers it talks to.
Electron hardening
contextIsolation: true,nodeIntegration: falseon the renderer.- Preload exposes a narrow
window.huskAPI; the renderer never seesrequire,process, or Node globals. Content-Security-Policyrestricts the renderer:script-src 'self'(no inline scripts, no remote scripts).style-src 'self' 'unsafe-inline'(we setstyle="..."from JS for dynamic UI; no remote styles).img-src 'self' data:,connect-src 'self',object-src 'none',frame-ancestors 'none'.
webSecurityis left at the default (on).- DevTools is disabled in packaged builds (
webPreferences.devTools: !app.isPackaged). The View menu entry,F12, andCtrl+Shift+Iare all no-ops in shipped installers. Source-tree runs (./run.sh) keep DevTools for development. - External URL clicks (plain-text URLs and OSC 8 hyperlinks emitted by the agent) route through a confirm dialog that shows the full URL before
shell.openExternalruns. The renderer cannot open a URL silently.
File permissions
~/.config/husk/is created with mode 0700.~/.config/husk/config.jsonis written with mode 0600.~/.claude.jsonand~/.copilot/mcp-config.json(which hold MCP env vars and HTTP headers, often API keys) are written with mode 0600. Husk re-chmods these files on every write to repair pre-existing loose permissions.
Path containment in IPC handlers
sessions:deleterejects any path that is not under~/.claude/projects/and does not end in.jsonl. It also requires confirmation via a native dialog before unlinking.context:removerefuses any path outside~/.claude/MEMORY/CONTEXT/.fs:dropFilerejects basenames containing.., slashes, or backslashes; resolves the destination and verifies it stays inside the target directory.skills:createvalidates the name against^[a-z][a-z0-9-]*$before creating a directory.mcp:addvalidates the server id against^[a-zA-Z0-9_-]+$and refuses to overwrite an existing entry.- Workflows imported from external JSON go through a sanitizer (
src/lib/workflow-graph.js) that drops unknown node types and rejects edges that point at non-existent nodes.
Process isolation
- The PTY child runs as the user, no elevation, no setuid.
- The spawn is per-platform (see
docs/architecture.mdfor the rationale). macOS spawns the agent binary directly with an argv array. Linux wraps with/usr/bin/scriptto establish a controlling terminal. Windows resolves the agent name through PATHEXT and spawns the executable directly when possible, falling back tocmd.exe /conly when no resolved path is found. None of these paths interpolate user-controlled text into a shell command line. - On window close or quit, Husk SIGTERMs the PTY's process group then SIGKILLs after a 250 ms grace period (
killPtyTree). A single-instance lock prevents accidental process pile-up. mcp:healthis dispatched through a per-agent adapter (src/lib/mcp/). The claude adapter shells out toclaude mcp listwith no user-controlled arguments; the copilot adapter reads the config file and reportsconfiguredwithout a live probe; other agents return an empty list.
Voice subsystem
- Piper (Linux) and
say(macOS) are spawned withspawn()and an argv array, notexec()with a shell string, so user-supplied text reaches the binary as a discrete argv entry and never through a shell.
Installer dependency pinning
install.shandinstall.ps1fetch any third-party installer (for example the bun script) to a temp file and verify its SHA-256 against a constant pinned in the script before executing. The helpers live ininstaller/lib/verify.shandinstaller/lib/verify.ps1. The pinning policy and how to refresh a pin are documented at the top of those files.
Release artifact integrity
- Every GitHub Release ships a
SHA256SUMSfile covering every binary in the release. - Every release artifact is signed with a Sigstore-backed build provenance attestation tied to the workflow run that produced it (via
actions/attest-build-provenance). Users can verify withgh attestation verify <file> --repo DorShaer/Husk. electron-builderis invoked with--publish neverso the release notes and uploads have a single writer (the release workflow), and the notes themselves come from the annotated git tag, not auto-generated text.
Dependencies
npm audit --audit-level=highruns on every push and pull request in.github/workflows/security.yml; a high or critical advisory blocks the PR.- GitHub Actions are pinned by commit SHA. Dependabot opens weekly grouped PRs for npm minor and patch, plus action minor and patch; majors are human-authored.
- The production dependency tree is intentionally small:
@xterm/*,node-pty,electron-updater,drawflow.
- No code signing or notarization on macOS. The mac
.dmgand.zipare unsigned. Gatekeeper will challenge them on first launch. See the README's "macOS first launch" section for the bypass. Apple Developer ID signing is on the roadmap; in-app auto-update on macOS is disabled until then and the update pill falls back to the releases page. - No SHA verification on Piper or voice-model downloads. The Piper tarball and
.onnxmodels come fromgithub.com/rhasspy/piperandhuggingface.coover HTTPS, but Husk does not pin a hash for them. A compromise of those release assets would be honored. - No renderer sandbox. Husk runs with
sandbox: falsebecause node-pty's preload needs it.contextIsolationand CSP still apply. - No encryption at rest of MCP secrets. MCP env vars and HTTP headers are stored in
~/.claude.jsonor~/.copilot/mcp-config.jsonat mode 0600 in plaintext, which matches the on-disk format of the agent CLIs themselves. If you need stronger protection, keep secrets in your OS keychain and reference them via env vars in the MCP entry. - No fine-grained permission prompts inside the agent's own tool calls. Husk does not interpose between the agent and its tool calls; tool gating is whatever the agent CLI provides.
Please do not file security issues as public GitHub issues.
Email: kassofer@gmail.com
Include:
- A short description.
- Steps to reproduce, ideally with a minimal Husk config or PTY input.
- The platform you tested on (Linux distro and version / macOS version / Windows version, Husk version, Node version).
- Whether you have a candidate fix.
You will get an acknowledgement within a few days, and credit in the release notes if you would like.