CodeVetter · Repo Unpack · Risk map
DietrichGebert/ponytail
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.
- 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.
- 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.
- 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.
- 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).
- 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.
- 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)
- 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.
- 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.
- 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)
- 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)
- 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.