# Risk map — DietrichGebert/ponytail

> Canonical page: https://codevetter.com/unpack/ponytail/risk-map

# Risk map — DietrichGebert/ponytail

Scanned commit e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156 (2026-09-14). Generated by codevetter codevetter 1.12.0 on 2026-09-22.

Primary risk surface is that ponytail code runs inside the user's home directory on every prompt: it writes state files under ~/.claude, ~/.cursor, ~/.qoder, plugin data dirs, and config dirs, and it edits user-owned settings.json/hooks.json files. Mitigations are real — a shell-safe path allowlist, BOM-tolerant parsing, fail-open hooks with hard timeouts, and surgical uninstalls — but the blast radius of a bug is the user's agent config, not the repo. Secondary risks: drift between 8 version manifests and 7+ rule copies (guarded), env-detection order fragility, and benchmark runs that shell out to claude with bypassPermissions.
- Hooks write outside the repo: setMode/clearMode mutate <stateDir>/.ponytail-active under ~/.claude, $PLUGIN_DATA, $COPILOT_PLUGIN_DATA, ~/.qoder, or ~/.cursor; writeDefaultMode mutates the user's global ponytail config.json; uninstall.js and cursor-hooks.js edit or delete ~/.cursor/hooks.json and ~/.claude/settings.json. — [hooks/ponytail-runtime.js#L31-48](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-runtime.js#L31-L48) [hooks/ponytail-config.js#L136-151](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-config.js#L136-L151) [scripts/uninstall.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/scripts/uninstall.js) [scripts/cursor-hooks.js#L24-47](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/scripts/cursor-hooks.js#L24-L47)
- Shell-injection guard: isShellSafe() allowlists path characters before the install path is embedded in a statusline shell command or a Cursor hooks.json entry; a checkout path with quotes/&/$/backtick/; falls back to manual-setup text instead of emitting a command. — [hooks/ponytail-config.js#L45-52](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-config.js#L45-L52) [hooks/ponytail-activate.js#L84-105](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-activate.js#L84-L105) [scripts/cursor-hooks.js#L53-65](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/scripts/cursor-hooks.js#L53-L65) [tests/hooks.test.js#L15-19](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/tests/hooks.test.js#L15-L19)
- Host detection is a single ordered cascade of env checks in ponytail-runtime.js; a host that sets a foreign env var (e.g. CLAUDE_PLUGIN_ROOT under VS Code Copilot, CURSOR_VERSION leaking into a nested Claude session) is misrouted — the file documents two such bugs already fixed (#528, #817), so new hosts must be inserted with care. — [hooks/ponytail-runtime.js#L8-29](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-runtime.js#L8-L29)
- Fail-silent posture: nearly every hook error path is an empty catch ('Silent fail — don't block session start'), which keeps sessions alive but means real breakage (unwritable state dir, EPIPE) leaves no signal beyond missing output. — [hooks/ponytail-activate.js#L54-58](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-activate.js#L54-L58) [hooks/ponytail-activate.js#L107-115](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-activate.js#L107-L115) [hooks/ponytail-mode-tracker.js#L139-141](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-mode-tracker.js#L139-L141) [hooks/ponytail-subagent.js#L26-29](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-subagent.js#L26-L29)
- A hung hook would freeze the user's prompt submission; both stdin-consuming hooks defend with an 'error' handler plus a 1s unref'd timeout that force-finishes, a workaround for Windows PowerShell swallowing piped stdin (#443). — [hooks/ponytail-mode-tracker.js#L147-155](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-mode-tracker.js#L147-L155) [hooks/ponytail-subagent.js#L75-77](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-subagent.js#L75-L77)
- settings.json/hooks.json edits are surgical but fragile: uninstall.js splits a combined statusLine command on '&&'/';' (documented ponytail: simplification — piped statuslines are unsupported), and cursor-hooks.js identifies ponytail entries only by the /ponytail-[\w-]+\.js/ regex on the command string. — [scripts/uninstall.js#L48-66](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/scripts/uninstall.js#L48-L66) [scripts/cursor-hooks.js#L18-22](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/scripts/cursor-hooks.js#L18-L22)
- Version drift has shipped before (v4.8.0 stayed at 4.7.0 across all manifests, #260/#262); check-versions.js now pins 8 files and validates the release tag, but it only runs in CI — a local tag push before CI could still publish stale manifests. (inferred) — [scripts/check-versions.js#L1-30](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/scripts/check-versions.js#L1-L30) [.github/workflows/publish.yml](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/.github/workflows/publish.yml)
- Rule-copy drift: seven per-host rule files must stay byte-identical to AGENTS.md and nine invariant phrases must exist in SKILL.md; check-rule-copies.js is a canary, not full equality for SKILL.md (noted in its own comment), so subtle ruleset wording changes can slip if they dodge the invariants. — [scripts/check-rule-copies.js#L39-69](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/scripts/check-rule-copies.js#L39-L69)
- The agentic benchmark deliberately runs `claude -p` with --permission-mode bypassPermissions on seeded repos and force-kills process trees on timeout — safe by design (temp dirs, Bash disallowed, per-session killpg/taskkill /T) but it is the only code path that executes an agent with full permissions. — [benchmarks/agentic/run.py#L225-244](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/benchmarks/agentic/run.py#L225-L244) [benchmarks/agentic/run.py#L310-336](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/benchmarks/agentic/run.py#L310-L336)
- Mode-filtering correctness is load-bearing: filterSkillBodyForMode drops SKILL.md lines whose table/example label is a mode name; a future SKILL.md edit that labels a normal rule 'full:'-style (without a quoted value) is safe, but the Python port in __init__.py uses a slightly different example regex (`:\s*` vs `:\s*"`), so the two filters can diverge on edge lines. (inferred) — [hooks/ponytail-instructions.js#L11-41](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-instructions.js#L11-L41) [__init__.py#L70-87](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/__init__.py#L70-L87)
- PONYTAIL_SUBAGENT_MATCHER is user-supplied regex compiled with 'i'; catastrophic backtracking on a hostile pattern is theoretically possible but bounded by the 1s timeout and fail-open design. (inferred) — [hooks/ponytail-subagent.js#L31-48](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-subagent.js#L31-L48)
- Cursor rule conflict is handled but detection only checks the first workspace root (CURSOR_PROJECT_DIR || cwd) — a documented ponytail: simplification; a ponytail.mdc in a secondary folder of a multi-root workspace goes undetected and would double-inject. — [hooks/ponytail-runtime.js#L59-70](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-runtime.js#L59-L70)

Every claim links to its cited files at the pinned commit. Citations identify source evidence; they do not establish runtime correctness. Independent CodeVetter scan — not reviewed by upstream maintainers.

## Public product links

- [CodeVetter](https://codevetter.com/)
- [Download](https://codevetter.com/download)
- [Documentation](https://codevetter.com/docs/)
- [Source](https://github.com/Codevetter/codevetter)
