For user-facing data-handling guarantees (no credentials stored, no telemetry, what AI subcommands send to Claude, etc.), see the Security & data handling section in the README. This document covers vulnerability reporting, threat model, and advisory history for security researchers and contributors.
If you discover a security issue, please open a private security advisory via the repo's Security tab. Do not file a public issue with exploit details.
Include in the report:
- Affected file(s) and line numbers, or a minimal reproduction.
- Impact (what an attacker gains, who the attacker has to be, what they have to control).
- Suggested fix, if you have one.
There is no bug bounty. Reports will be acknowledged within ~7 days; fixes typically ship within ~14 days for HIGH-severity findings.
The toolkit is a local CLI for a single user on a single workstation. The trust boundary is the user's UID.
In scope:
- Subprocess argument injection / shell injection in calls to
gh,git,yq. - Path traversal in user-supplied or batch-file-supplied paths under
notes_root/and~/.claude/work-plan/. plan-statusreads, and with its action flags writes, plan/spec docs inside the repo it is pointed at (--repo=<key>resolved from config, or cwd). All writes are confined to docs discovered under that repo root and are opt-in:--stamprewrites an idempotent status block in discovered plan docs.--archivegit mvs dead plans within the repo; source/dest are repo-relative (discover_docs+ a pure path join), so a declared path cannot redirect a move outside the tree.--issuesopens GitHub issues via list-formgh issue create(no shell); the repo slug is config-sourced and titles/bodies are positional args, so plan content cannot injectghflags or shell commands.--llmis a two-step pass mirroringsuggest-priorities: batch + answers live in~/.claude/work-plan/cache/(mode 0700), and--applyvalidates provenance — it rejects a batch whoserepo_rootdiffers from the current repo and any answer whoserelwas not in the prepared batch, so an attacker-planted answers file cannot inject a write path.- File paths declared inside a plan flow to
git logonly after a--pathspec separator and via list-formsubprocess, so a hostile path in a plan doc cannot injectgitflags or shell commands.
- Viewer-driven frontmatter writes (
plan-confirm/plan-ack/plan-baseline, #286) write exactly one key (verdict_override/acknowledged/verdict_baseline) into a plan doc's YAML frontmatter — never the body/checkboxes/manifest — through the sharedlib/plan_fmpath. The targetrelis resolved byresolve_doc_path, which requires a real file resolving under the repo root (an absolute path, a../-escape, or an in-repo symlink whose target is outside is rejected), and a write to a public repo is held behind theneeds_confirmtoken (fails CLOSED on unknown visibility).frontmatter.write_fileadditionally refuses to write through a symlink. close-issue(#305) is a GitHub-mutating call (gh issue close). The issue number is coerced viastr(int(...)), the repo is validatedowner/namebefore anyghcall, the close reason is whitelisted to{completed, not_planned}, and args are list-form (no shell) — so neither a hostile number, repo, nor comment can injectghflags or shell commands. The VS Code viewer gates every close behind a mandatory confirm modal.push-track(#306) writes a track file into the repo's shared.work-plan/worktree andgit pushes the plan branch. The destination is a repo-relative path inside the ensured worktree (no traversal); the private copy is removed only after the shared copy is written (no data-loss window); and the push is held behind theneeds_confirmtoken on a public repo (the world-visible exposure), firing before any mutation.- Cross-UID attacks: another local user (or a same-UID malicious process) planting state files the toolkit reads back. The cache directory at
~/.claude/work-plan/cache/is mode 0700, and batch state files have validated provenance fields. - YAML / markdown frontmatter parsing edge cases routed through
yq. Config-write expressions pass user values toyqas opaquestrenv()/env()data, never interpolated into the expression, so a value containing"or yq operators cannot break out. - Frontmatter-supplied git revisions (
github.branches) are rejected if dash-led and passed togitonly in list form, so a hostile branch name cannot inject agitoption (e.g.--output=). - The VS Code extension (
vscode/) spawns the CLI as a subprocess.workPlan.cliPathis machine-scoped — a workspace.vscode/settings.jsoncannot redirect the spawned executable — the extension declaresuntrustedWorkspaces: { supported: false }and does not spawn in an untrusted workspace, and GitHub-derived positionals are passed after a--end-of-options separator.
Out of scope:
- Attackers who already control the user's shell, install path, or
ghauth token. The toolkit is downstream of those trust roots. - Network-level attacks against
ghitself (it has its own threat model). - Supply-chain attacks against Python,
gh,git, oryqbinaries. - Denial of service / resource exhaustion (the toolkit runs interactively under user control). As hardening, every
gh/gitsubprocess is bounded by a timeout, so a hung network call or pathological repo can't stall the CLI — or the VS Code extension that spawns it — indefinitely.
main is the only supported branch. There is no LTS series. Fixes ship to main and re-installing via ./install.sh or .\install.ps1 picks them up.
| Date | Severity | Summary | Fix |
|---|---|---|---|
| 2026-04-29 | HIGH | Path traversal in group._apply() via attacker-planted batch file |
#19 (closed #18) |
| 2026-04-29 | MEDIUM | /tmp/ symlink-following allowed cross-UID file overwrite |
#19 |
| 2026-04-29 | MEDIUM | Unvalidated repo from batch file flowed to gh issue edit |
#19 |
| 2026-06-10 | HIGH | yq expression injection in set-notes-root: a path containing " broke out of the yq string and rewrote arbitrary config.yml keys |
#199 (closed #191) |
| 2026-06-10 | HIGH | git option injection via dash-led github.branches frontmatter → arbitrary file overwrite (git log --output=) |
#199 (closed #192) |
| 2026-06-10 | HIGH | VS Code extension: workspace-overridable workPlan.cliPath spawned on activation + no Workspace Trust declaration → code execution on opening a malicious workspace |
#198 (closed #193) |
| 2026-06-10 | MEDIUM | Argument injection via ---prefixed track names (no -- end-of-options separator before positionals) |
#198 / #199 (closed #194) |
| 2026-06-10 | LOW | Filesystem hardening: init write-containment, new-track folder validation, symlink-write guard |
#199 (closed #195) |
| 2026-06-10 | LOW | Subprocess hardening: gh/git timeouts, centralized repo-slug validation, yq strenv()/env(), answers int-coercion |
#199 (closed #196) |
| 2026-06-10 | LOW | VS Code webview/extension hardening: move handler routed through the confirm modal, Mermaid label newline neutralization, escaper/CSP nits | #198 (closed #197) |
All three 2026-04-29 findings shared a common root cause (predictable /tmp/ paths used as inter-invocation state for the two-step AI subcommands). The fix moved state to ~/.claude/work-plan/cache/ (mode 0700) and added validation on user-controlled fields read back from the batch.
The 2026-06-10 batch came from a full security review of the CLI and the VS Code extension. The two HIGH injection findings (#191, #192) were reproduced end-to-end before fixing; the extension RCE (#193) was a configuration-hardening fix (machine-scoped cliPath + Workspace Trust). The list-argv discipline throughout the CLI meant no classic shell-injection existed — the real classes were yq-expression injection, git-option injection, and the extension's process-spawn trust boundary.