Please use GitHub's private vulnerability reporting for this repository: https://github.com/earthwalker17/MiniDSH/security/advisories/new. Do not open a public issue for a defect that can be exploited.
Include what you can of: the version (minidsh --version), the operating system, the enforcement value on the session's opening sandbox/mode event (minidsh sessions show <id> --json carries it; --audit prints it for a confinable mode), and the session log if one is involved — minidsh sessions show <id> --json, or the file under ~/.minidsh/sessions/. Logs carry credential names, never values, but read a log before attaching it. This is a small project with one maintainer; expect an acknowledgement within a week.
The latest 1.x release.
MiniDSH is a coding agent that edits files and runs commands on your machine, so it is worth being precise about what is a security boundary and what is not. The authoritative description is docs/ARCHITECTURE.md §7 (authority) and §13 (known limitations); this is the summary.
- The model is never the boundary. It proposes; policy decides; the effect boundary enforces; every decision is a durable event in the session log.
- File writes are fenced inside the
fsprovider, in process, against the session's recorded sandbox mode: canonicalize, contain, refuse before any effect. Reads are unrestricted by design. The fence follows symbolic links and cannot follow a hard link, so a hard link inside the workspace whose inode lives outside it lets a write through. That is a documented limitation, not an undisclosed one. - Shell commands are confined by the operating system where a backend exists — bubblewrap on Linux, Seatbelt on macOS — and the backend is probed functionally before the harness claims it. Windows has no backend, so by default the harness refuses to run a command confined there: the model asks for a one-shot escalation and a person grants it (
--approvegrants them all on a headless run). A session may instead record that it accepts weaker enforcement (--accept none, or/accept none), which runs the shell unconfined under the session's own mode with the in-process file fence intact — a durable, per-session, replayable fact. Onserve,webandchatthe flag is the deployment's default, and a weakened default is written into every session it opens, for each confinable mode, so a stored log says what it accepted: a line, orfullby its absence. Confinement governs file effects only, and by path: a confined command writes through a hard link that already sits in the workspace, like the file fence (measured under bubblewrap and Seatbelt); neither backend lets it create one across the boundary. A confined command still reaches the network. Its environment is scrubbed of the credential names this deployment declared and ofMINIDSH_*, and of nothing else, but that is defence in depth, not a boundary: reads are never fenced, so~/.minidsh/credentials.jsonstays readable, and any secret the deployment did not declare passes through. - A background job runs under the authority its start resolved — the session's mode and recorded acceptance, never an escalation — and a durable switch of either ends it. It is stopped when its session ends; a job process that outlives a killed MiniDSH process (on macOS, and unconfined on Linux, where bubblewrap does not take it down) runs unsupervised, and the next resume reports it lost without stopping it. On Windows each PowerShell the harness starts holds what it starts in a kill-on-close Job of its own, so a job's processes die with it and with a killed host; a process created through WMI or the Task Scheduler, or one that asks to break away from the Job, escapes, and so does everything where Constrained Language Mode refuses the containment (a warning says so).
- An approval says what the runtime will do. The line a person answers carries a subject the harness builds from the validated call arguments — the command, and the sandbox mode and enforcement it would run under — plus the whole call as the log recorded it. The model's own justification appears beside that, never in place of it. A yes may be extended to that exact action for the rest of that run of the session; such a consent is a durable event, it is offered by the host rather than invented by a surface, it dies the moment any authority knob moves or the run ends (a resume or a fork asks again), and it can be revoked.
- A network tool (the opt-in
web_search) asks before every query leaves the machine, on a line naming the query, where it goes and the credential it goes as; that consent is the only authority over egress — nothing confines or records the request — and a delegated child is not offered the tool. - The browser tools (on by default where Chrome, Edge or Chromium is installed) drive a headless browser the harness starts over a pipe, with a fresh profile and downloads refused. Inside the browser, a proxy the harness owns and resolver rules confine it to localhost, the workspace's own files and any
allowHoststhe row names, and a canary proves that fence each time it starts. That fence is the browser's own, not the shell's, and it is drawn around the internet, not around this machine: the browser reaches every service listening on localhost, under every sandbox mode and from a delegated child, without asking and without a record, and such a service may act on its own authority. It asks no consent by design, so a verifier child can use it. - Skills are data, never authority. A repository's
.agents/skillsor.claude/skillsis listed to the model like any file it might read: a skill's text can steer the model, its scripts run as ordinary shell commands under the session's authority, and it never replaces a skill of yours or a bundled one. On Linux and macOS a.gitabove the working directory that another user owns ends the search for repository skills andAGENTS.mdat the working directory. - A delegated child runs under a ceiling it cannot widen; the ceiling is recorded at its creation and enforced before dispatch.
- Provider keys stay out of what the harness writes: a key is redacted from any provider or proxy error before that error reaches the log, the model or stderr, and a base URL carrying a username or password is refused.
minidsh webbinds loopback by default and exchanges a one-shot launch token for a signed, HttpOnly, SameSite=Strict cookie behind a Host/Origin fence, and refuses to be framed by another page. That is reachability, not identity: whoever can open the URL is the one principal. Binding to a non-loopback address is the operator's decision.composition.jsonand any module-loaded plugin are code-equivalent trust. The harness warns about authority-sensitive rows a layer changed; it does not sandbox them.- Model-written text that reaches a terminal is sanitized where it is recorded (approval reasons, at the approval seam) and where it is rendered (
src/app/present.ts).
Things that would be vulnerabilities, and that we would like to hear about: a write that lands outside the workspace on a confined host without an approval, other than through a hard link that already existed in the workspace, or a file left outside by another process swapping a directory for a link mid-write (only ever empty on Linux, best effort elsewhere; both documented, ARCHITECTURE §13); a browser that reaches a non-loopback host its row did not allow; a provider key in a log, an error or a tool result; an approval whose recorded subject or reason differs from what the person was shown, or from what the call actually runs; two different actions that share one standing consent; a query that leaves the machine without the consent its line showed, or with characters the line did not show; a standing consent that survives a narrowing of the session's authority; a way to reach the web surface without the cookie; a session log that records less authority than was exercised; a way for a delegated child to widen its ceiling; a way for repository content (an AGENTS.md, a file the model reads) to change what the harness is allowed to do rather than what the model is told.