# Behavior traces — DietrichGebert/ponytail

> Canonical page: https://codevetter.com/unpack/ponytail/behavior-traces

# Behavior traces — DietrichGebert/ponytail

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

Three traced flows: (1) session activation — host fires SessionStart → ponytail-activate.js resolves mode → writes flag → emits filtered ruleset; (2) mid-session mode switch — UserPromptSubmit → ponytail-mode-tracker.js parses stdin → updates flag/config → confirms; (3) subagent spawn — SubagentStart → ponytail-subagent.js reads flag → optionally filters on agent_type → injects. Also traced: OpenCode turn injection and the Cursor hooks install/uninstall path.
- Startup flow (Claude Code): claude-codex-hooks.json SessionStart spawns `node hooks/ponytail-activate.js` → getDefaultMode() resolves env→config→'full' → 'off' exits early after clearMode() → setMode(mode) writes $CLAUDE_CONFIG_DIR/.ponytail-active → getPonytailInstructions(mode) builds the ruleset → statusline check appends a one-time setup nudge → writeHookOutput('SessionStart', mode, output). — [hooks/claude-codex-hooks.json#L3-15](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/claude-codex-hooks.json#L3-L15) [hooks/ponytail-activate.js#L28-115](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-activate.js#L28-L115)
- Cursor startup differs: ponytail-activate.js first checks cursorRulePath() for .cursor/rules/ponytail.mdc in CURSOR_PROJECT_DIR; if the always-on rule exists it emits only a cursorRuleNotice (no flag write, no ruleset) so hook and rule can't contradict. — [hooks/ponytail-activate.js#L38-51](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-activate.js#L38-L51) [hooks/ponytail-runtime.js#L59-78](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-runtime.js#L59-L78)
- Mode-switch flow: UserPromptSubmit → ponytail-mode-tracker.js accumulates stdin → finish() JSON-parses (BOM-stripped) → /^[/@$]ponytail/ match → 'default' arg calls writeDefaultMode and returns early; a level arg calls setMode + emits 'PONYTAIL MODE CHANGED'; 'off' calls clearMode + 'PONYTAIL MODE OFF'; bare /ponytail is report-only. — [hooks/ponytail-mode-tracker.js#L21-107](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-mode-tracker.js#L21-L107)
- Deactivation flow: isDeactivationCommand requires the whole trimmed message to be 'stop ponytail' or 'normal mode' (after dropping trailing punctuation), so an incidental phrase like 'add a normal mode toggle' cannot turn ponytail off — regression-tested in tests/hooks.test.js. — [hooks/ponytail-config.js#L36-43](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-config.js#L36-L43) [hooks/ponytail-mode-tracker.js#L109-114](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-mode-tracker.js#L109-L114) [tests/hooks.test.js#L110-125](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/tests/hooks.test.js#L110-L125)
- Subagent flow: SubagentStart → ponytail-subagent.js → readMode() returns flag contents; absent/'off' exits 0 silently → optional PONYTAIL_SUBAGENT_MATCHER compiled in try/catch (invalid → inject-everywhere) → if matcher set, stdin agent_type tested; definite mismatch exits, everything else → inject() → writeHookOutput('SubagentStart', mode, ruleset). — [hooks/ponytail-subagent.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-subagent.js)
- Never-hang contract: both stdin-reading hooks attach stdin 'error' → finish()+exit and a 1000ms unref'd setTimeout fallback, because Windows PowerShell `if {}` wrappers can swallow piped JSON so 'end' never fires (#443); the no-matcher subagent path never touches stdin at all. — [hooks/ponytail-mode-tracker.js#L144-155](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-mode-tracker.js#L144-L155) [hooks/ponytail-subagent.js#L41-48](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-subagent.js#L41-L48) [hooks/ponytail-subagent.js#L73-77](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-subagent.js#L73-L77)
- OpenCode turn flow: plugin config() hook scans .opencode/command/*.md via parseCommandFile into config.command and appends skills/ to config.skills.paths → per turn, experimental.chat.system.transform reads ~/.config/opencode/.ponytail-active (falling back to getDefaultMode) and appends getPonytailInstructions(mode) to the system prompt → command.execute.before writes the new mode when the user runs /ponytail. — [.opencode/plugins/ponytail.mjs#L51-98](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/.opencode/plugins/ponytail.mjs#L51-L98) [.opencode/plugins/ponytail-frontmatter.cjs#L13-21](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/.opencode/plugins/ponytail-frontmatter.cjs#L13-L21)
- pi session flow: session_start → getDefaultMode + getHideStatus + resolveSessionMode(entries) → syncStatus paints '🐴 ponytail: <icon> <MODE>' → before_agent_start returns systemPrompt with ruleset appended (guard against null event/systemPrompt, #439/#440) → agent_start/agent_end toggle the ●/○ active dot. — [pi-extension/index.js#L183-210](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/pi-extension/index.js#L183-L210) [pi-extension/index.js#L71-99](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/pi-extension/index.js#L71-L99)
- Cursor install flow: scripts/cursor-hooks.js install → readConfig (malformed JSON → SyntaxError → refuse) → stripPonytail removes stale ponytail entries → ponytailEntries() renders cursor-hooks.json with PONYTAIL_DIR→forward-slash root after isShellSafe() rejects metachar paths → merged entries appended per event → writeConfig; uninstall reverses and deletes the file if nothing else remains. — [scripts/cursor-hooks.js#L44-105](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/scripts/cursor-hooks.js#L44-L105)
- Cleanup flow: scripts/uninstall.js removes .ponytail-active from the Claude and Cursor dirs, the config file, strips ponytail entries from ~/.cursor/hooks.json via cursorHooks.uninstall('user'), and surgically removes only the ponytail segment from a possibly-combined statusLine command in settings.json. — [scripts/uninstall.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/scripts/uninstall.js)
- Agentic benchmark flow: run.py run_cell copies the fixture repo (or writes seed files) → git snapshot → spawns `claude -p <prompt> --model <m> --permission-mode bypassPermissions --disallowedTools Bash --strict-mcp-config` (+ --plugin-dir for skill arms) → stdout→_claude.json file (never a PIPE, so timeouts work) → 300s CELL_TIMEOUT with _tree_kill → score_workspace computes git_diff_stats/code_stats + per-task deterministic scorers. — [benchmarks/agentic/run.py#L283-339](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/benchmarks/agentic/run.py#L283-L339)

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)
