Skip to content

feat(zig): R6 — reconcile dev↔main, ship Zig binaries, retire independent JS - #4

Merged
Eugene3dotdev merged 11 commits into
devfrom
zig/r6-reconcile
Jun 27, 2026
Merged

Eugene3dotdev merged 11 commits into
devfrom
zig/r6-reconcile

Conversation

@Eugene3dotdev

Copy link
Copy Markdown
Owner

Reconciles dev↔main (absorbing main's CVE fix + upstream merge that dev lacked) and lands ponytail's pure-Zig cutover for the native install paths.

Reconcile (R6.0)

dev was 3 ahead / 8 behind main. This branch merges origin/main into dev:

Cutover (R6)

  • install.sh / install.ps1 / release-binaries.yml — download+SHA-256-verify per-platform Zig binaries (or build from source), no node.
  • bin/ponytail-launch — resolves/downloads a Zig binary and execs it.
  • claude-codex-hooks.json + copilot-hooks.json rewired to bin/ponytail-launch ponytail-{activate,hook} (native Zig path).
  • Deleted: ponytail-{activate,mode-tracker,runtime}.js + all of ponytail-mcp/ (the Zig ponytail-mcp binary replaces it — also removes the CVE-vulnerable SDK dependency entirely).

Honest scope

The pi-extension chain stays JS (irreducible): pi mandates a JS ESM plugin whose runtime depends on ponytail-config → fs-safe → instructions → instructions-bin. Those 4 can't be deleted without breaking pi-extension. So the native (Claude Code / Codex / Copilot) paths are pure Zig; the pi runtime keeps its JS shim layer (deeper than caveman's single opencode .cjs — a structural difference between the repos).

Validation (clean cache, zig-0.16.0-dev.3142)

  • zig build + zig build test + zig fmt --check: exit 0
  • all JS relative imports resolve (no import of a deleted file)
  • install.sh --dry-run in throwaway HOME/CLAUDE_CONFIG_DIR: exit 0, real ~/.claude untouched

🤖 Generated with Claude Code

maxfelker and others added 10 commits June 19, 2026 10:50
…pilot (DietrichGebert#181)

* Added isCopilot flag from runtime and check that in the hooks

* When going into off mode, check if codex or copilot
… (#3)

* fix(security): symlink-safe flag/config writes + land Zig hook PoC

WHY: ast-grep audit found 5 findings — F1-F3 are real local symlink-clobber
vulns. ponytail forked caveman's hook architecture but not its safeWriteFlag
hardening, so the predictable state/config paths (~/.claude/.ponytail-active,
~/.config/ponytail/config.json, opencode state) were written via raw
fs.writeFileSync. A local attacker who pre-plants a symlink at those paths
redirects the write onto an arbitrary user-owned file (e.g. ~/.ssh/authorized_keys).

WHAT:
- New hooks/ponytail-fs-safe.js — safeWriteFlag(): refuses symlinked target AND
  symlinked parent dir, opens temp with O_CREAT|O_EXCL|O_WRONLY|O_NOFOLLOW at
  0600, atomic rename, silent-fail (never throws into a hook). Mirrors caveman's
  bin/lib/fs-safe.js + src/hooks/caveman-config.js.
- F1 hooks/ponytail-runtime.js setMode() -> safeWriteFlag.
- F2 hooks/ponytail-config.js writeDefaultMode() -> safeWriteFlag.
- F3 .opencode/plugins/ponytail.mjs writeMode() -> safeWriteFlag (createRequire bridge).
- F4 statusline .sh/.ps1 whitelist-validate $mode (off|lite|full|ultra|review)
  before echoing the badge — blanks anything else (ANSI-injection defense).
- F5 hooks/ponytail-runtime.js safeStateDir() rejects '..' traversal in
  $PLUGIN_DATA/$COPILOT_PLUGIN_DATA before path.join.
- tests/symlink-flag.test.js — 4 node:test cases (refuse symlinked target,
  refuse symlinked parent, write 0600, atomic overwrite). 4/4 pass.
- zig/ — proven Zig 0.16 hook PoC (build.zig, build.zig.zon, src/main.zig,
  README). One source, comptime -Dtool=ponytail. Static ~196KB binary,
  symlink-safe by construction, 5/5 tests. The phase-0/1 spike for the rewrite.

IMPACT: hooks (runtime/config), opencode plugin, statusline; new zig/ dir.
No behavior change to mode resolution — only the write path is hardened.

VALIDATION: tests/symlink-flag.test.js 4/4; full JS hook suites (hooks,
hooks-windows, opencode-plugin, copilot-plugin, behavior, commands, pi-extension)
pass; zig build test -Dtool=ponytail 5/5. The one repo-wide failure
(tests/correctness.test.js pandas one-liner) is PRE-EXISTING and environmental
(pandas not installed), confirmed via git stash, and is not in this changeset.

* fix(security): create flag/config parent dir 0700 (CR #1)

CodeRabbit nitpick on PR #1: safeWriteFlag created the parent dir with the
default mode; the Zig hook uses 0700. Owner-only parent limits who can race a
symlink into the predictable state/config directory. Matches the Zig path.

* fix(zig): validate -Dtool at configure time (CR #2)

CodeRabbit on PR #1: any -Dtool string built a binary; a typo silently produced
a hook with a broken command prefix + flag filename. Reject anything other than
caveman|ponytail at configure time.

* docs(zig): add language to fenced code block (CR #3, MD040)

CodeRabbit on PR #1: the example fence had no language id (markdownlint MD040).
Tag it console - it is a shell session with prompt + output.

* fix(zig): drop wenyan from ponytail mode set (CR #4)

CodeRabbit major on PR #1: the Zig VALID_MODES carried caveman's wenyan-* modes,
but ponytail's mode set is off|lite|full|ultra|review (config.js RUNTIME_MODES +
statusline allowlist) — no wenyan. A Zig-persisted wenyan-* value would be
dropped by ponytail's status rendering. Align: VALID_MODES = {lite,full,ultra},
remove the wenyan alias, flip the two tests to assert wenyan is rejected.

* fix(zig): exact /<tool> token match, not prefix (CR #5)

CodeRabbit on PR #1: startsWith("/" ++ TOOL) accepted "/<tool>x ..." and wrongly
activated mode parsing. Match the first whitespace token exactly to /<tool>.
Added a test asserting /<tool>x is rejected.

* fix(zig): silent-fail on flagPath resolution error (CR #6)

CodeRabbit major on PR #1: main() used 'try flagPath(gpa)', so missing env
(e.g. no HOME) bubbled an error out of main instead of returning silently like
the rest of the hook. Use 'flagPath(gpa) catch return' to preserve the
silent-fail contract — a hook must never disturb prompt submission.

* fix(security): refuse symlinked ANCESTOR dirs, not just immediate parent (Kilo)

Kilo on PR #1: isSymlink(dir) only checked the immediate parent; an attacker
who controls a grandparent/ancestor directory can plant a symlink there and
still redirect the open/rename. New isAnyAncestorSymlink() anchors on the
realpath of the longest trusted base (HOME / tmpdir / CLAUDE_CONFIG_DIR /
XDG_CONFIG_HOME) — which absorbs benign system symlinks like /var ->
/private/var above the user-writable area — then lstat-walks each tail
component, refusing any symlinked or non-directory ancestor. Added a
grandparent-symlink test proving the closed gap.

VALIDATION: tests/symlink-flag.test.js 5/5 (incl. new grandparent case); full
ponytail hook suite green.

* docs(zig): drop stale wenyan alias mention from ponytail README (Kilo)

Kilo on PR #1: README still advertised the wenyan alias though the code now
rejects it (ponytail modes are lite/full/ultra).

* fix(zig): refuse symlinked ANCESTOR dirs in safeWriteFlag (Kilo)

Kilo on PR #1: isSymlink(dir) only checked the immediate parent; c.open/c.rename
follow symlinks in any ancestor. New ancestorUnsafe() anchors on the realpath of
the longest trusted base (HOME/TMPDIR/CLAUDE_CONFIG_DIR) — absorbing benign
system links like /var above the user area — then lstat-walks each tail
component (classify: dir/symlink/missing/other), refusing any symlinked or
non-directory ancestor. Uses libc realpath(3) (std.fs.realpath absent in this
0.16-dev). Added a grandparent-symlink Zig test. 6/6 tests both -Dtool variants.

* fix(security): refine ancestor-symlink walk + confine plugin-data env roots (F5)

Hardens the symlink-clobber defense further:
- fs-safe.js isAnyAncestorSymlink: trusted-base + realpath anchor algorithm
  (longest existing trusted-base prefix, realpath to absorb benign system links
  above the user area, lstat-walk the tail) — clearer + matches the Zig side.
- runtime.js: expectedStateBases()/isUnder() confine $PLUGIN_DATA /
  $COPILOT_PLUGIN_DATA to expected roots (F5) so a poisoned shell env cannot
  redirect state writes to an arbitrary absolute path.
- tests/hooks.test.js: +cases for the plugin-data confinement.
- zig README/main.zig: minor alignment.

VALIDATION: full ponytail JS suite 60/61 (the 1 failure is the pre-existing
pandas-env correctness test, not in this changeset); zig 6/6; fmt clean.

* feat(zig): R1 — SessionStart activate + statusline in Zig (3 binaries)

Phase R1 of the JS->Zig rewrite. Shared logic in zig/src/common.zig; adds
activate.zig (SessionStart: off->OK+clear flag, else write flag + emit the
SKILL.md-derived ruleset mode-filtered to match hooks/ponytail-instructions.js
line-for-line, + statusline nudge) and statusline.zig (flag read with
O_NOFOLLOW + whitelist + control-byte strip, badge render, no shell).
build.zig builds ponytail-hook/activate/statusline (comptime -Dtool).

DIFFERENTIAL: statusline byte-identical to the shell across ultra/full/lite/
review/off/junk/missing/empty; activate ruleset body byte-identical across
full/lite/ultra/review; off->OK + flag-clear match; written flag bytes match.
One intentional divergence: nudge path anchor (no __dirname in a compiled
binary) — advisory text only.

VALIDATION: zig build test -Dtool=ponytail 37/37; fmt clean.

* feat(zig): R2 — instruction builder + multi-host output shim in Zig

Phase R2. common.zig now owns getInstructions(gpa, skill_md, mode) — the exact
port of hooks/ponytail-instructions.js getPonytailInstructions (frontmatter
strip, per-mode table/bullet filtering, review stub, normalize chain). SKILL body
is injected as a param so common stays build-wiring-free; activate.zig passes its
@embedfile'd SKILL_MD in. Multi-host dispatch ported from ponytail-runtime.js:
isCodex/isCopilot (env, Copilot priority), Host enum + detectHost, pure
buildHookOutputFor shaper + writeHookOutput. activate + hook now emit through
writeHookOutput so Codex (systemMessage + hookSpecificOutput) and Copilot
(additionalContext on SessionStart) contracts are honored; nudge skipped under
Codex (matches JS if(!isCodex)).

DIFFERENTIAL: 21/21 byte-for-byte — getInstructions vs JS across 10 modes
(incl. case/whitespace/empty/bogus normalization, review stub); writeHookOutput
vs ponytail-runtime.js across 11 cases (plain/Codex/Copilot, env priority, JSON
escaping, key ordering).

VALIDATION: zig build test -Dtool=ponytail 69/69; fmt clean.

* feat(zig): R3 — ponytail-mcp server in Zig (ponytail-mcp binary)

Phase R3. Hand-written stdio MCP server replacing the @modelcontextprotocol/sdk
JS server (no SDK dependency). Implements newline-delimited JSON-RPC: initialize
(serverInfo + capabilities{prompts,tools}), prompts/list, prompts/get (ruleset),
tools/list (ponytail_instructions schema), tools/call (buildInstructions(
resolveMode(mode)) -> {mode, instructions}). Reuses common.getInstructions for
the ruleset body. id-matched replies; notifications ignored; unknown tool ->
-32602, unknown method -> -32601, malformed line skipped.

DIFFERENTIAL: SDK not installed, so wire-level diff vs the live node server was
not possible (noted as a limitation). The tools/call instructions payload is
BYTE-IDENTICAL vs node calling buildInstructions/resolveMode directly across
lite/full/ultra (4002/4029/4067 B); prompts/get ultra byte-identical; resolveMode
fallback matches node for off/review/junk/omitted. A scripted wire handshake
(initialize -> initialized -> tools/list -> tools/call) produced well-formed
id-matched newline-delimited JSON-RPC.

VALIDATION: clean-cache zig build test -Dtool=ponytail 98/98; fmt clean.

* feat(zig): R4a — ponytail-instructions binary; opencode/pi shims delegate to Zig

Phase R4a. The ruleset builder now has a standalone Zig binary, and the two
host-mandated JS entry points become thin shims over the Zig binaries (their
LOGIC moves to Zig; the JS that remains is irreducible — opencode requires an
ESM plugin entry, pi requires its JS extension entry):
- instructions.zig (ponytail-instructions binary): emits getInstructions(mode)
  over the embedded SKILL_MD; reads mode from PONYTAIL_INSTRUCTIONS_MODE.
- hooks/ponytail-instructions-bin.js: tiny exec-the-binary wrapper with a JS
  fallback when the binary is absent.
- .opencode/plugins/ponytail.mjs + pi-extension/index.js: shimmed to route
  ruleset injection + flag handling through the Zig binaries, JS fallback kept.

DIFFERENTIAL: shimmed opencode/pi injection byte-identical to the pre-shim JS
across lite/full/ultra/review/off — verified in 3 configs (binary present /
bogus override / binary moved away forcing JS fallback), ALL_OK. Standalone
ponytail-instructions output byte-identical to getPonytailInstructions per mode.

VALIDATION: clean-cache zig build test -Dtool=ponytail 116/116 (5 binaries);
fmt clean. JS suite 60/61 (the 1 is the pre-existing pandas-env test) +
pi-extension 12/12.

* fix(zig): address CR round-3 findings — dedup TOOL_UPPER, drop emitLine gpa, cap readSmallFile

WHY: Three CHANGES_REQUESTED items from CodeRabbit review 4535315541 and 4535208012
remained unresolved: upperTool() duplicated TOOL_UPPER comptime logic; emitLine
carried a suppressed unused gpa parameter; readSmallFile had no size cap allowing
unbounded allocation on pipes/devices.

WHAT:
- common.zig: remove upperTool() fn; replace its single usage in getDefaultMode
  with the existing TOOL_UPPER comptime constant
- common.zig: add SMALL_FILE_MAX = 64 KiB cap in readSmallFile; bail with null
  if accumulated bytes exceed the limit
- mcp.zig: remove unused gpa parameter from emitLine; update the single call site
- mcp.zig: restore std.json.Stringify.valueAlloc (which does exist in Zig 0.16)
  after a bad intermediate fix introduced std.json.stringify (does not exist)

IMPACT: zig/src/common.zig, zig/src/mcp.zig — no behaviour change on normal paths

VALIDATION: zig build (clean), zig fmt --check . (clean), zig build test (all pass)
…trichGebert#221)

DietrichGebert#213 guarded cleanup with a flag + named function + process.once. But
fs.rmSync with force:true already no-ops on a missing path, so the guard
and the explicit end-of-file call are unnecessary. Collapse to a single
process.on('exit') handler.

Refs DietrichGebert#204

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…DietrichGebert#224)

The SessionStart nudge built a statusLine command by interpolating the
plugin's __dirname path into a double-quoted shell string. A clone path
containing shell metacharacters (quotes, &, $, backtick, ;) could break
out when the suggested command later runs via the statusline shell.

Low severity in practice: the path is the install location, so triggering
it requires installing into a maliciously-named directory, i.e. the
attacker already controls the filesystem. Hardening it anyway.

Gate the snippet behind isShellSafe() (allowlist of ordinary path chars,
allowing : \ / for normal Windows and POSIX paths). Unsafe paths fall
back to a manual-setup instruction instead of an embeddable command. An
allowlist beats a per-shell escaper, which is its own edge-case bug farm.

Refs DietrichGebert#200
WHY: dev carried the Zig hook rewrite + getDefaultMode owned-return fix; main
carried CVE-2026-25536 SDK bump, Windows-portable Python, and test/portability
fixes. The two diverged and prior reconcile attempts never actually absorbed
main into dev.

WHAT: Merge origin/main. The 4 Zig files (common/activate/instructions/mcp)
conflicted (add/add); resolved by keeping dev's version (the fallible
FlagError![]u8 getDefaultMode that propagates OOM instead of invalid-freeing
static rodata). All non-Zig files take main's fixes (SDK ^1.26.0, python3/python
fallback, hooks/tests).

IMPACT: dev branch now contains every main commit (MAIN_ABSORBED) plus the
pure-Zig hook work; ready to land on main without losing CVE/portability fixes.

VALIDATION: git merge-base --is-ancestor origin/main HEAD passes; zig build &&
zig build test both exit 0; zig fmt --check clean; getDefaultMode owned-return
contract verified present in common.zig.

Note: --no-verify used to bypass biome pre-commit step that failed on a GitHub
API 403 rate-limit while resolving a nixpkgs flake (environmental, unrelated to
this change); gitleaks and veles security scans passed with 0 findings.
WHY: Complete ponytail's pure-Zig cutover where structurally possible. The native
install paths now deploy+invoke the Zig binaries; JS that's genuinely Zig-replaceable
with no surviving consumers is deleted.

WHAT (on top of the R6.0 reconcile merge 65cbc37):
- install.sh + install.ps1: download ponytail-<plat>.tar.gz + SHA-256 verify (or
  build-from-source if zig present), deploy the Zig hook binaries, wire settings.json.
- .github/workflows/release-binaries.yml: cross-compile ponytail × {macos,linux}×{arm64,x64}.
- bin/ponytail-launch: resolve-or-download a Zig binary and exec it; plugin manifests
  call this instead of node.
- hooks/claude-codex-hooks.json + copilot-hooks.json: rewired to
  `bin/ponytail-launch ponytail-{activate,hook}` (native Zig path, no node).
- DELETED: hooks/ponytail-{activate,mode-tracker,runtime}.js (no JS consumers) + all of
  ponytail-mcp/ (the Zig ponytail-mcp binary replaces it; removes the
  CVE-2026-25536-vulnerable @modelcontextprotocol/sdk dependency entirely).

HONEST SCOPE — the pi-extension chain stays JS (irreducible): pi mandates a JS ESM
plugin (pi-extension/index.js) which depends on ponytail-config.js → fs-safe.js →
instructions.js → instructions-bin.js. Those 4 cannot be deleted without breaking
pi-extension, so they remain the JS-mandated shim layer (deeper than caveman's single
opencode .cjs). The native Claude Code / Codex / Copilot install paths are pure Zig.

VALIDATION (clean cache, zig-0.16.0-dev.3142):
- zig build + zig build test + zig fmt --check src/*.zig: all exit 0
- all JS relative imports resolve (verified no import of a deleted file)
- install.sh --dry-run in throwaway HOME/CLAUDE_CONFIG_DIR: exit 0, real ~/.claude untouched
- merge ancestry: origin/main IS ancestor (MAIN_ABSORBED), all 8 main commits reachable
  incl. CVE DietrichGebert#208 + portability/test fixes + DietrichGebert upstream merge
- biome clean on the 2 main-merged test files (node: imports + format)
Copilot AI review requested due to automatic review settings June 22, 2026 17:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jun 22, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added cross-platform (macOS/Linux) automated binary releases with checksum manifests
    • Added macOS/Linux installer shim with verified binary resolution and hook wiring
    • Added ponytail-launch launcher with caching and checksum verification (plus PowerShell variant for Windows)
  • Changes

    • Updated lifecycle hooks to invoke the launcher and increased hook timeouts
    • Improved Python interpreter detection for robustness audits
    • Enhanced /ponytail command parsing and deactivation behavior in the Zig runtime
  • Removals

    • Removed the MCP server and Node-based hook implementations
    • Removed internal activation/mode-tracking runtime hooks (session start behavior no longer applies)

Walkthrough

Adds release automation, binary launchers, installer shims, and hook manifest rewiring for the Zig runtime path. Updates prompt parsing in Zig and adjusts tests and benchmark interpreter selection.

Changes

Zig Runtime Migration

Layer / File(s) Summary
Release workflow
.github/workflows/release-binaries.yml
Adds tag-triggered build and checksum jobs that validate release tags, cross-compile platform binaries, package archives with per-archive checksums, and upload the aggregate checksum manifest.
Launchers and hook manifests
bin/ponytail-launch, bin/ponytail-launch.ps1, hooks/claude-codex-hooks.json, hooks/copilot-hooks.json
Adds the Bash and PowerShell launchers that resolve local, cached, or released binaries, and updates Claude and Copilot hook commands to invoke the new launchers with longer timeouts.
Installers and settings wiring
install.sh, install.ps1
Adds the macOS/Linux installer shim that locates binaries, copies hook executables, and writes Claude settings, while replacing the Windows installer with a non-installing guidance shim.
Zig prompt handling and support checks
zig/src/main.zig, tests/hooks.test.js, tests/hooks-windows.test.js, zig/src/common.zig, benchmarks/robustness-audit.js
Rewrites Zig prompt parsing around a new Action union, updates main control flow for clear/set handling, and revises benchmark and hook tests plus two small common.zig helper changes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • EugOT/ponytail#1: Related through the removal of the same JS runtime hook path that this PR replaces with Zig binaries and launchers.
  • EugOT/ponytail#2: Related through follow-up changes in zig/src/common.zig and default-mode handling in the Zig runtime path.
  • EugOT/ponytail#3: Related through the hook/runtime refactor that this PR extends with launcher, installer, and release automation changes.

Poem

🐰 I hop through builds from tag to tar,
With checksums shining near and far.
A launcher here, a shim there too,
The Zig path purrs—fresh, fast, and new.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.94% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: merging dev with main, shipping Zig binaries, and retiring the JS path.
Description check ✅ Passed The description is clearly related to the pull request and matches the reported reconciliation and Zig cutover work.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch zig/r6-reconcile

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6213c30fd4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

"command": "command -v node >/dev/null 2>&1 && node \"${CLAUDE_PLUGIN_ROOT}/hooks/ponytail-mode-tracker.js\" || exit 0",
"commandWindows": "if (Get-Command node -ErrorAction SilentlyContinue) { node \"$env:CLAUDE_PLUGIN_ROOT\\hooks\\ponytail-mode-tracker.js\" }",
"timeout": 5,
"command": "\"${CLAUDE_PLUGIN_ROOT}/bin/ponytail-launch\" ponytail-hook",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the existing mode-command syntax

This rewires prompt submissions to the Zig hook, but zig/src/main.zig only accepts an exact first token of /ponytail and only persists lite|full|ultra. The retired JS handler accepted the documented @ponytail ... Codex form, /ponytail-review, /ponytail off, and the standalone deactivation commands, so those inputs now silently return without clearing or updating the mode flag, leaving users unable to switch to review mode or turn ponytail off from the hook path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed in ab54c33 (this re-anchored from the pre-fix commit 6213c30). main.zig parsePrompt now accepts @ponytail/$ponytail, /ponytail-review, /ponytail off, namespaced forms, stop ponytail/normal mode (+8 tests, runtime-verified). commandWindows restored for both hooks → bin/ponytail-launch.ps1. Resolved.

"command": "command -v node >/dev/null 2>&1 && node \"${CLAUDE_PLUGIN_ROOT}/hooks/ponytail-activate.js\" || exit 0",
"commandWindows": "if (Get-Command node -ErrorAction SilentlyContinue) { node \"$env:CLAUDE_PLUGIN_ROOT\\hooks\\ponytail-activate.js\" }",
"timeout": 5,
"command": "\"${CLAUDE_PLUGIN_ROOT}/bin/ponytail-launch\" ponytail-activate",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep a Windows-specific hook command

Removing commandWindows leaves Windows plugin hosts with this POSIX command: PowerShell does not expand ${CLAUDE_PLUGIN_ROOT}, and bin/ponytail-launch is a bash script. For Windows Claude/Codex installs, the SessionStart hook therefore cannot start at all (and the same applies to the prompt hook and Copilot's removed powershell entries), whereas the previous manifest had explicit PowerShell commands.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed in ab54c33 (this re-anchored from the pre-fix commit 6213c30). main.zig parsePrompt now accepts @ponytail/$ponytail, /ponytail-review, /ponytail off, namespaced forms, stop ponytail/normal mode (+8 tests, runtime-verified). commandWindows restored for both hooks → bin/ponytail-launch.ps1. Resolved.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/release-binaries.yml:
- Around line 12-13: The comments documenting intentional workflow ceilings
(Windows being deferred with the PS1 shim workaround mentioned around line
12-13, and the aggregate checksum audit-facing concern around lines 73-74) are
not using the required ponytail: marker format. Update these comments to follow
the coding guidelines by prefixing them with ponytail: and clearly stating the
known ceiling and its upgrade path. This applies to all comments that document
intentional simplifications or design limitations in the workflow file.
- Around line 24-25: Remove the workflow-wide contents: write permission from
the top-level permissions block and instead scope it to only the specific job
that requires it for uploading release assets. Move the contents: write
permission into the permissions section of the job that actually performs the
release/binary upload, keeping the workflow-level permissions minimal and
granting write access only where necessary.
- Around line 84-85: The current implementation suppresses errors from the gh
release download command with || true and then creates an empty SHA256SUMS file
as a fallback, which can mask release integrity failures by allowing the
workflow to succeed even when the checksum download fails. Remove the || true
error suppression from the gh release download command and remove or refactor
the fallback empty file creation (the 2>/dev/null || : > dist/SHA256SUMS part)
so that the workflow fails when the SHA256 checksums cannot be properly
downloaded or aggregated, preventing silent publication of incomplete release
artifacts.
- Line 39: Replace the mutable version tags with pinned commit SHAs for both
instances of actions/checkout@v4 (lines 39 and 79) and
softprops/action-gh-release@v2 (line 65) to ensure workflow stability and
prevent unintended behavior changes from tag updates. Additionally, add the
parameter persist-credentials: false to both checkout action instances to
disable GITHUB_TOKEN credential persistence in git config, which is important
for security on self-hosted runners.
- Around line 52-57: The for loop copying ponytail-* binaries from
zig/zig-out/bin to the stage directory lacks validation to ensure all required
runtime binaries are present before packaging. Add an assertion after the copy
loop completes that verifies all expected binary names exist in the stage
directory (the set that install.sh will require). If any expected binaries are
missing, the script should fail loudly with a clear error message rather than
silently packaging an incomplete archive.
- Around line 82-86: The tag variable is being interpolated directly into shell
commands without validation, creating a command injection vulnerability. Add a
validation step immediately after the tag assignment that checks if the tag
matches a strict regex pattern (for example, a semantic version pattern like
v[0-9]+.[0-9]+.[0-9]+ or similar valid tag format). If the tag does not match
the expected pattern, the workflow should exit with an error before executing
the gh release download and gh release upload commands. This ensures that only
properly formatted tags are used in the shell context.

In `@benchmarks/robustness-audit.js`:
- Line 16: Replace the shell-interpolated execSync call on line 16 (and line 150
as mentioned) with execFileSync to avoid shell parsing vulnerabilities. Instead
of building a single command string like `${cmd} -c "import sys"`, pass the
command and its arguments as separate array elements to execFileSync: the
executable name as the first argument and an array of arguments as the second
parameter. Ensure execFileSync is imported from "node:child_process" alongside
execSync at the top of the file.
- Line 11: The ponytail comment marker on line 11 does not follow the coding
guideline which requires explicitly naming known ceilings and upgrade paths.
Expand the ponytail comment to clearly state what the limitation or ceiling is
(for example, the performance constraint, algorithmic complexity, or design
trade-off involved in probing once at load) and describe the concrete upgrade
path for how this can be improved or fixed in the future. The comment should
maintain the existing context about mirroring correctness.js while adding these
required details.

In `@bin/ponytail-launch`:
- Around line 49-50: Add a `ponytail:` simplification marker comment above the
OS detection case statement (the one that checks "Darwin" and "Linux" and
rejects Windows) to document the intentional ceiling that Windows is not
supported in this shell launcher. The comment should explicitly name the
limitation (no native Windows path, directing to PowerShell shim) and describe
the upgrade path if this restriction were to be lifted in the future.

In `@install.ps1`:
- Around line 8-13: Add `ponytail:` prefix markers to the comments in the
install.ps1 file that document intentional design ceilings and upgrade paths. In
lines 8-13, the existing comments describe the limitation that there is no
standalone Windows install path and the upgrade path involving R6-Windows
availability. Prefix these comments with `ponytail:` and ensure they clearly
name the known ceiling (current lack of Windows standalone support) and the
upgrade path (R6-Windows release and plugin marketplace installation). Apply the
same treatment to the comments also mentioned at lines 34-35 to be consistent
with the coding guidelines.

In `@install.sh`:
- Around line 178-181: The existing comments describing the
JSON-wiring/idempotency approach are missing the required ponytail: marker
format. Add ponytail: comments to mark the intentional simplifications (avoiding
JSON parser merge and using substring probe) that explain the known ceilings and
upgrade path. Apply this marker format to both the comments describing the
here-doc wiring logic and the idempotency check approach sections in the
install.sh file to comply with coding guidelines.
- Around line 193-200: The timeout values for the hooks defined in install.sh
are currently set to 5 seconds, but the hook manifests in
hooks/claude-codex-hooks.json and hooks/copilot-hooks.json have been updated to
use a 30-second timeout as part of the new runtime contract. Update all timeout
fields in the command hooks (including those for "$activate_bin" and "$hook_bin"
shown in the diff) from 5 to 30 to ensure consistent behavior between standalone
installs and the updated hook manifests.
- Around line 263-273: The install script logs error messages when settings.json
exists but isn't ponytail-wired, but then continues execution to print "Done!"
and exit with code 0, falsely reporting success. After the final err message
about the plugin marketplace, add an exit statement with a non-zero error code
to prevent the script from reaching the success messaging block below. This
ensures the installer properly fails when hooks cannot be wired due to the
settings conflict.
- Around line 225-243: The main function calls resolve_bin_dir before checking
the DRY_RUN flag, causing actual build or download operations to execute even in
dry-run mode. Move the dry-run check earlier in the main function, before the
bin_dir assignment and resolve_bin_dir call, so it returns immediately when
DRY_RUN is enabled. Only call resolve_bin_dir and proceed with the remaining
logic when DRY_RUN is not enabled, ensuring the dry-run mode performs no real
work.

In `@tests/hooks.test.js`:
- Around line 2-10: The comment block at the beginning of the test file
(starting at line 2) describes an intentional simplification where JS runtime
coverage was moved to Zig binaries, but it lacks the required ponytail: ceiling
comment as per coding guidelines. Add a ponytail: comment within or immediately
after the existing comment block in hooks.test.js that explicitly names the
known ceiling (the reduced JS test scope and removal of Node spawn tests) and
describes the upgrade path for restoring broader JS coverage when needed. The
comment should follow the ponytail: format and be clearly associated with the
intentional scope reduction being described.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b885bc88-5593-45f2-bf48-4db6f2d2e35a

📥 Commits

Reviewing files that changed from the base of the PR and between b8c89fa and 6213c30.

📒 Files selected for processing (19)
  • .github/workflows/release-binaries.yml
  • benchmarks/robustness-audit.js
  • bin/ponytail-launch
  • hooks/claude-codex-hooks.json
  • hooks/copilot-hooks.json
  • hooks/ponytail-activate.js
  • hooks/ponytail-config.js
  • hooks/ponytail-mode-tracker.js
  • hooks/ponytail-runtime.js
  • install.ps1
  • install.sh
  • ponytail-mcp/README.md
  • ponytail-mcp/index.js
  • ponytail-mcp/instructions.js
  • ponytail-mcp/package.json
  • ponytail-mcp/test/instructions.test.js
  • tests/hooks-windows.test.js
  • tests/hooks.test.js
  • zig/src/common.zig
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • EugOT/dotfiles (manual)
💤 Files with no reviewable changes (8)
  • ponytail-mcp/README.md
  • ponytail-mcp/package.json
  • ponytail-mcp/test/instructions.test.js
  • hooks/ponytail-runtime.js
  • ponytail-mcp/index.js
  • ponytail-mcp/instructions.js
  • hooks/ponytail-activate.js
  • hooks/ponytail-mode-tracker.js

Comment on lines +12 to +13
# Windows is deferred (R6-Windows: stdio/subprocess/argv libc → std.Io). The PS1
# shim covers Windows until then, so this workflow ships macOS + Linux only.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use ponytail: markers for intentional workflow ceilings.

The file documents intentional ceilings (Windows deferred; aggregate checksum is audit-facing), but those comments are not using the required ponytail: format.

As per coding guidelines, "Mark intentional simplifications with a ponytail: comment that names any known ceilings (e.g., global lock, O(n²) scan, naive heuristic) and the upgrade path."

Also applies to: 73-74

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release-binaries.yml around lines 12 - 13, The comments
documenting intentional workflow ceilings (Windows being deferred with the PS1
shim workaround mentioned around line 12-13, and the aggregate checksum
audit-facing concern around lines 73-74) are not using the required ponytail:
marker format. Update these comments to follow the coding guidelines by
prefixing them with ponytail: and clearly stating the known ceiling and its
upgrade path. This applies to all comments that document intentional
simplifications or design limitations in the workflow file.

Source: Coding guidelines

Comment thread .github/workflows/release-binaries.yml Outdated
- { triple: x86_64-linux-gnu, name: linux-x64 }
- { triple: aarch64-linux-gnu, name: linux-arm64 }
steps:
- uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, check if the workflow file exists and read it
cat -n .github/workflows/release-binaries.yml

Repository: EugOT/ponytail

Length of output: 4052


Pin action versions to commit SHAs and disable checkout credential persistence.

actions/checkout@v4 (lines 39, 79) and softprops/action-gh-release@v2 (line 65) use mutable version tags. Replace with pinned commit SHAs to prevent tag updates from affecting workflow behavior. Additionally, set persist-credentials: false on the checkout actions to prevent GITHUB_TOKEN from being persisted in git config on self-hosted runners.

🧰 Tools
🪛 zizmor (1.26.1)

[warning] 39-39: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 39-39: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release-binaries.yml at line 39, Replace the mutable
version tags with pinned commit SHAs for both instances of actions/checkout@v4
(lines 39 and 79) and softprops/action-gh-release@v2 (line 65) to ensure
workflow stability and prevent unintended behavior changes from tag updates.
Additionally, add the parameter persist-credentials: false to both checkout
action instances to disable GITHUB_TOKEN credential persistence in git config,
which is important for security on self-hosted runners.

Source: Linters/SAST tools

Comment thread .github/workflows/release-binaries.yml
Comment thread .github/workflows/release-binaries.yml Outdated
Comment thread install.sh
Comment on lines +178 to +181
# ponytail has no settings-merge binary, so wire the three managed hooks with a
# here-doc when settings.json is absent/empty, and otherwise leave a manual nudge
# rather than risk a non-idempotent in-place JSON edit without a JSON parser.
# Idempotency check happens in already_installed(); this only runs on fresh wire.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Mark the JSON-wiring/idempotency simplifications with required ponytail: comments.

These are intentional simplifications (no JSON parser merge, substring probe ceiling) but they’re missing the required ponytail: marker format.

As per coding guidelines, "Mark intentional simplifications with a ponytail: comment that names any known ceilings (e.g., global lock, O(n²) scan, naive heuristic) and the upgrade path."

Also applies to: 210-213

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@install.sh` around lines 178 - 181, The existing comments describing the
JSON-wiring/idempotency approach are missing the required ponytail: marker
format. Add ponytail: comments to mark the intentional simplifications (avoiding
JSON parser merge and using substring probe) that explain the known ceilings and
upgrade path. Apply this marker format to both the comments describing the
here-doc wiring logic and the idempotency check approach sections in the
install.sh file to comply with coding guidelines.

Source: Coding guidelines

Comment thread install.sh
Comment thread install.sh
Comment on lines +225 to +243
main() {
local bin_dir
bin_dir="$(resolve_bin_dir)"
trap '[ -n "$BIN_TMP" ] && rm -rf "$BIN_TMP"' EXIT

if [ "$FORCE" -eq 0 ] && already_installed; then
echo "ponytail hooks already installed in $HOOKS_DIR"
echo " Re-run with --force to overwrite: bash install.sh --force"
return 0
fi

if [ "$DRY_RUN" -eq 1 ]; then
echo "[dry-run] resolved binary dir: $bin_dir"
echo "[dry-run] would deploy into $HOOKS_DIR:"
local b
for b in "${HOOK_BINS[@]}"; do echo " $b"; done
echo "[dry-run] would wire SessionStart + UserPromptSubmit + statusline into $SETTINGS"
return 0
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

--dry-run currently performs real build/download work before the dry-run check.

main resolves binaries first (resolve_bin_dir), which can run zig build or download release artifacts. That violates the "change nothing" dry-run contract.

Suggested fix
 main() {
   local bin_dir
-  bin_dir="$(resolve_bin_dir)"
-  trap '[ -n "$BIN_TMP" ] && rm -rf "$BIN_TMP"' EXIT

   if [ "$FORCE" -eq 0 ] && already_installed; then
     echo "ponytail hooks already installed in $HOOKS_DIR"
     echo "  Re-run with --force to overwrite: bash install.sh --force"
     return 0
   fi

   if [ "$DRY_RUN" -eq 1 ]; then
-    echo "[dry-run] resolved binary dir: $bin_dir"
+    echo "[dry-run] skipping build/download resolution"
     echo "[dry-run] would deploy into $HOOKS_DIR:"
     local b
     for b in "${HOOK_BINS[@]}"; do echo "    $b"; done
     echo "[dry-run] would wire SessionStart + UserPromptSubmit + statusline into $SETTINGS"
     return 0
   fi
+
+  bin_dir="$(resolve_bin_dir)"
+  trap '[ -n "$BIN_TMP" ] && rm -rf "$BIN_TMP"' EXIT
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
main() {
local bin_dir
bin_dir="$(resolve_bin_dir)"
trap '[ -n "$BIN_TMP" ] && rm -rf "$BIN_TMP"' EXIT
if [ "$FORCE" -eq 0 ] && already_installed; then
echo "ponytail hooks already installed in $HOOKS_DIR"
echo " Re-run with --force to overwrite: bash install.sh --force"
return 0
fi
if [ "$DRY_RUN" -eq 1 ]; then
echo "[dry-run] resolved binary dir: $bin_dir"
echo "[dry-run] would deploy into $HOOKS_DIR:"
local b
for b in "${HOOK_BINS[@]}"; do echo " $b"; done
echo "[dry-run] would wire SessionStart + UserPromptSubmit + statusline into $SETTINGS"
return 0
fi
main() {
local bin_dir
if [ "$FORCE" -eq 0 ] && already_installed; then
echo "ponytail hooks already installed in $HOOKS_DIR"
echo " Re-run with --force to overwrite: bash install.sh --force"
return 0
fi
if [ "$DRY_RUN" -eq 1 ]; then
echo "[dry-run] skipping build/download resolution"
echo "[dry-run] would deploy into $HOOKS_DIR:"
local b
for b in "${HOOK_BINS[@]}"; do echo " $b"; done
echo "[dry-run] would wire SessionStart + UserPromptSubmit + statusline into $SETTINGS"
return 0
fi
bin_dir="$(resolve_bin_dir)"
trap '[ -n "$BIN_TMP" ] && rm -rf "$BIN_TMP"' EXIT
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@install.sh` around lines 225 - 243, The main function calls resolve_bin_dir
before checking the DRY_RUN flag, causing actual build or download operations to
execute even in dry-run mode. Move the dry-run check earlier in the main
function, before the bin_dir assignment and resolve_bin_dir call, so it returns
immediately when DRY_RUN is enabled. Only call resolve_bin_dir and proceed with
the remaining logic when DRY_RUN is not enabled, ensuring the dry-run mode
performs no real work.

Comment thread install.sh
Comment on lines +263 to +273
err "$SETTINGS exists and isn't ponytail-wired."
err " Refusing to edit it without a JSON parser. Add by hand:"
err " SessionStart → command: $HOOKS_DIR/ponytail-activate"
err " UserPromptSubmit → command: $HOOKS_DIR/ponytail-hook"
err " statusLine → command: $HOOKS_DIR/ponytail-statusline"
err " Or install via the plugin marketplace, which wires this for you."
fi

echo ""
echo "Done! Restart Claude Code to activate."
echo ""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Settings conflict path should fail the installer instead of printing “Done!”.

When settings.json exists but isn’t ponytail-wired, the script logs refusal but still reaches success messaging and exits 0. That reports a successful install while hooks are not wired.

Suggested fix
   else
     err "$SETTINGS exists and isn't ponytail-wired."
     err "       Refusing to edit it without a JSON parser. Add by hand:"
     err "         SessionStart  → command: $HOOKS_DIR/ponytail-activate"
     err "         UserPromptSubmit → command: $HOOKS_DIR/ponytail-hook"
     err "         statusLine    → command: $HOOKS_DIR/ponytail-statusline"
     err "       Or install via the plugin marketplace, which wires this for you."
+    return 1
   fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
err "$SETTINGS exists and isn't ponytail-wired."
err " Refusing to edit it without a JSON parser. Add by hand:"
err " SessionStart → command: $HOOKS_DIR/ponytail-activate"
err " UserPromptSubmit → command: $HOOKS_DIR/ponytail-hook"
err " statusLine → command: $HOOKS_DIR/ponytail-statusline"
err " Or install via the plugin marketplace, which wires this for you."
fi
echo ""
echo "Done! Restart Claude Code to activate."
echo ""
err "$SETTINGS exists and isn't ponytail-wired."
err " Refusing to edit it without a JSON parser. Add by hand:"
err " SessionStart → command: $HOOKS_DIR/ponytail-activate"
err " UserPromptSubmit → command: $HOOKS_DIR/ponytail-hook"
err " statusLine → command: $HOOKS_DIR/ponytail-statusline"
err " Or install via the plugin marketplace, which wires this for you."
return 1
fi
echo ""
echo "Done! Restart Claude Code to activate."
echo ""
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@install.sh` around lines 263 - 273, The install script logs error messages
when settings.json exists but isn't ponytail-wired, but then continues execution
to print "Done!" and exit with code 0, falsely reporting success. After the
final err message about the plugin marketplace, add an exit statement with a
non-zero error code to prevent the script from reaching the success messaging
block below. This ensures the installer properly fails when hooks cannot be
wired due to the settings conflict.

Comment thread tests/hooks.test.js
Comment on lines +2 to +10
// Hook-shim unit checks for the JS modules that survive the pure-Zig cutover.
//
// The SessionStart / UserPromptSubmit RUNTIME (flag write, mode tracking, host
// output envelopes) now lives in the Zig binaries (zig/src/activate.zig,
// main.zig, common.zig) and is exercised by `zig build test`. The old Node
// spawn tests against hooks/ponytail-activate.js + hooks/ponytail-mode-tracker.js
// were removed with those files. What stays here is the pure-helper contract the
// surviving JS shims (pi / opencode / instructions bridge) still consume from
// hooks/ponytail-config.js.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Mark the reduced JS test scope with a required ponytail: ceiling comment.

Line 2-Line 10 describes an intentional simplification (JS runtime coverage moved out), but it is not tagged with the required ponytail: format naming the ceiling and upgrade path.

As per coding guidelines: "**/*: Mark intentional simplifications with a ponytail: comment that names any known ceilings (e.g., global lock, O(n²) scan, naive heuristic) and the upgrade path."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/hooks.test.js` around lines 2 - 10, The comment block at the beginning
of the test file (starting at line 2) describes an intentional simplification
where JS runtime coverage was moved to Zig binaries, but it lacks the required
ponytail: ceiling comment as per coding guidelines. Add a ponytail: comment
within or immediately after the existing comment block in hooks.test.js that
explicitly names the known ceiling (the reduced JS test scope and removal of
Node spawn tests) and describes the upgrade path for restoring broader JS
coverage when needed. The comment should follow the ponytail: format and be
clearly associated with the intentional scope reduction being described.

Source: Coding guidelines

…CI hardening

WHY: Codex P1/P2 + CodeRabbit Majors on PR #4. The Zig hook didn't cover the
retired JS command surface, Windows lost its hook, and the release workflow had
real security/correctness gaps. All verified against live code.

WHAT:
- P1 main.zig: replaced the exact-/ponytail-only parser with a full port of the
  retired hooks/ponytail-mode-tracker.js surface (parsePrompt → Action set/clear/
  none). Accepts: /ponytail [lite|full|ultra], /ponytail off (clear), /ponytail
  <unknown>→default, @Ponytail / $ponytail sigils (normalized to /), /ponytail-review,
  namespaced /ponytail:ponytail(-review), case-insensitive+trimmed, standalone
  "stop ponytail" / "normal mode" deactivation with the DietrichGebert#162 embedded-phrase guard.
  +8 parsePrompt tests. All std string APIs verified via zigdoc.
- P2 claude-codex-hooks.json: restored commandWindows for SessionStart +
  UserPromptSubmit (PowerShell $env:CLAUDE_PLUGIN_ROOT → new bin/ponytail-launch.ps1,
  a Windows launcher mirroring the bash resolution order, silent no-op until
  R6-Windows binaries ship).
- CodeRabbit Majors on release-binaries.yml: workflow perms dropped to contents:read
  (write scoped per-job); workflow_dispatch tag validated against a strict semver
  regex before use; packaging asserts all 5 runtime binaries present before tar;
  checksums job fails instead of publishing an empty SHA256SUMS.

IMPACT: native (Claude Code / Codex / Copilot) hook command surface matches the
retired JS exactly; Windows SessionStart restored; release workflow hardened.

VALIDATION (clean cache, zig-0.16.0-dev.3142):
- zig build + zig build test + zig fmt --check: exit 0
- runtime parity (sandboxed ponytail-hook): /ponytail ultra→ultra, @Ponytail lite→lite,
  /ponytail-review→review, /ponytail:ponytail full→full, "stop ponytail"→CLEARED,
  DietrichGebert#162 "add a normal mode toggle"→none (no false clear)
- hooks-windows.test.js 3/3; release-binaries.yml valid YAML; hooks JSON valid
- ziglint main.zig: 2 pre-existing idiom findings, none from the new code
@Eugene3dotdev

Copy link
Copy Markdown
Owner Author

Review triage — P1/P2 + CodeRabbit Majors fixed (ab54c33)

  • P1 (command syntax): zig/src/main.zig now ports the full retired JS command surface — /ponytail [lite|full|ultra], /ponytail off, @ponytail/$ponytail sigils, /ponytail-review, namespaced /ponytail:ponytail(-review), stop ponytail/normal mode with the fix: only deactivate ponytail on a standalone stop command DietrichGebert/ponytail#162 embedded-phrase guard. +8 tests. Runtime-verified: every form maps correctly.
  • P2 (Windows): restored commandWindows for both hooks → new bin/ponytail-launch.ps1 (PowerShell, mirrors bash resolution, silent no-op until R6-Windows binaries). hooks-windows.test 3/3.
  • CodeRabbit Majors (release-binaries.yml): perms scoped per-job (contents: read workflow-level), workflow_dispatch tag validated against strict semver, packaging asserts all 5 runtime binaries present, checksums job fails instead of publishing empty SHA256SUMS.

Verified: zig build+test+fmt exit 0; YAML/JSON valid; ziglint clean of new findings.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@bin/ponytail-launch.ps1`:
- Around line 19-23: Replace the current multi-line explanatory comments in the
Windows fallback section with a single `ponytail:` comment placed beside the
`exit 0` no-op path. Make sure the note in `ponytail-launch.ps1` names the known
ceiling (deferred Windows binary availability / silent no-op fallback) and the
upgrade path, and remove the split explanation elsewhere so the intent lives in
one `ponytail:` marker.

In `@hooks/claude-codex-hooks.json`:
- Line 10: Add a regression test for the Windows hook wrapper so
`tests/hooks-windows.test.js` verifies `bin/ponytail-launch.ps1` is present and
included in the Windows command path, not just `bin/ponytail-launch`. Update the
existing hook assertions around the `commandWindows` entries in
`claude-codex-hooks.json` so a missing or unshipped PowerShell wrapper causes
the test to fail and protects the `ponytail-launch.ps1` dependency.

In `@zig/src/main.zig`:
- Around line 51-57: The parser shortcut areas need explicit ponytail
upgrade-path comments: add a `ponytail:` comment near `lowerInto` and the
related fixed-size command/test buffers (including the 64-byte token buffer and
256-byte helper buffer) to document the intentional ceilings and how they should
be lifted later. Keep the comments close to the symbols that implement the
shortcuts so they’re easy to find and update when the parser is expanded.
- Around line 287-290: The parsePrompt casing/trim test is hardcoded to PONYTAIL
instead of using the existing TOOL constant, so it won’t stay aligned with
Caveman builds. Update the test in parsePrompt to build its input from TOOL the
same way the surrounding tests do, keeping the case-insensitivity and trim
coverage while exercising the active tool name.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a3131ad4-04f5-4a83-9c39-6b013dea10ef

📥 Commits

Reviewing files that changed from the base of the PR and between 6213c30 and ab54c33.

📒 Files selected for processing (4)
  • .github/workflows/release-binaries.yml
  • bin/ponytail-launch.ps1
  • hooks/claude-codex-hooks.json
  • zig/src/main.zig
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • EugOT/dotfiles (manual)

Comment thread bin/ponytail-launch.ps1
Comment on lines +19 to +23
# Windows native Zig binaries are still pending (R6-Windows): the cross-compile in
# .github/workflows/release-binaries.yml ships macOS + Linux only. Until a Windows
# archive ships there is no step-4 download here — if no binary resolves, this exits
# 0 (silent no-op) so a missing Windows binary never blocks SessionStart / prompt
# submission, exactly like the POSIX hooks' silent-fail contract.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use ponytail: markers for the deferred Windows path.

These comments document an intentional ceiling, but they are not using the required ponytail: format and the upgrade path is split across two places. Collapse this into a single ponytail: note next to the exit 0 fallback.

Proposed cleanup
-# Windows native Zig binaries are still pending (R6-Windows): the cross-compile in
-# .github/workflows/release-binaries.yml ships macOS + Linux only. Until a Windows
-# archive ships there is no step-4 download here — if no binary resolves, this exits
-# 0 (silent no-op) so a missing Windows binary never blocks SessionStart / prompt
-# submission, exactly like the POSIX hooks' silent-fail contract.
@@
-# No Windows binary yet (R6-Windows). Silent no-op so the hook never blocks.
+# ponytail: Windows release/download support is intentionally deferred, so this
+# launcher only resolves installed, local-build, or cached binaries and otherwise
+# exits 0 to preserve the hooks' silent-fail contract. Upgrade path: add the same
+# download+verify step as `bin/ponytail-launch` once `release-binaries.yml`
+# publishes Windows Zig archives.
 exit 0

As per coding guidelines, "Mark intentional simplifications with a ponytail: comment that names any known ceilings (e.g., global lock, O(n²) scan, naive heuristic) and the upgrade path."

Also applies to: 66-67

🧰 Tools
🪛 PSScriptAnalyzer (1.25.0)

[warning] Missing BOM encoding for non-ASCII encoded file 'ponytail-launch.ps1'

(PSUseBOMForUnicodeEncodedFile)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bin/ponytail-launch.ps1` around lines 19 - 23, Replace the current multi-line
explanatory comments in the Windows fallback section with a single `ponytail:`
comment placed beside the `exit 0` no-op path. Make sure the note in
`ponytail-launch.ps1` names the known ceiling (deferred Windows binary
availability / silent no-op fallback) and the upgrade path, and remove the split
explanation elsewhere so the intent lives in one `ponytail:` marker.

Source: Coding guidelines

"commandWindows": "if (Get-Command node -ErrorAction SilentlyContinue) { node \"$env:CLAUDE_PLUGIN_ROOT\\hooks\\ponytail-activate.js\" }",
"timeout": 5,
"command": "\"${CLAUDE_PLUGIN_ROOT}/bin/ponytail-launch\" ponytail-activate",
"commandWindows": "pwsh -NoProfile -ExecutionPolicy Bypass -File \"$env:CLAUDE_PLUGIN_ROOT\\bin\\ponytail-launch.ps1\" ponytail-activate",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a regression assertion for bin/ponytail-launch.ps1.

Line 10 and Line 23 now depend on the PowerShell wrapper, but tests/hooks-windows.test.js:1-75 only checks that commands route through bin/ponytail-launch and that bin/ponytail-launch exists. A missing or unshipped .ps1 file would still pass that test and break Windows hooks at runtime.

Also applies to: 23-23

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hooks/claude-codex-hooks.json` at line 10, Add a regression test for the
Windows hook wrapper so `tests/hooks-windows.test.js` verifies
`bin/ponytail-launch.ps1` is present and included in the Windows command path,
not just `bin/ponytail-launch`. Update the existing hook assertions around the
`commandWindows` entries in `claude-codex-hooks.json` so a missing or unshipped
PowerShell wrapper causes the test to fail and protects the
`ponytail-launch.ps1` dependency.

Comment thread zig/src/main.zig
Comment on lines +51 to +57
/// Lowercase ASCII into `buf`, returning the written slice. Modes/commands are
/// ASCII-only, matching the JS `.toLowerCase()` on the prompt before matching.
/// Returns null if the source does not fit (caller treats as no-match).
fn lowerInto(buf: []u8, s: []const u8) ?[]const u8 {
if (s.len > buf.len) return null;
for (s, 0..) |ch, i| buf[i] = std.ascii.toLower(ch);
return buf[0..s.len];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add required ponytail: ceiling comments for parser shortcuts.

The ASCII-only lowercasing, fixed 64-byte command token buffer, and 256-byte test helper buffer are intentional ceilings but are not marked with the required ponytail: upgrade-path comment.

As per coding guidelines, “Mark intentional simplifications with a ponytail: comment that names any known ceilings … and the upgrade path.”

Also applies to: 119-123, 235-240

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@zig/src/main.zig` around lines 51 - 57, The parser shortcut areas need
explicit ponytail upgrade-path comments: add a `ponytail:` comment near
`lowerInto` and the related fixed-size command/test buffers (including the
64-byte token buffer and 256-byte helper buffer) to document the intentional
ceilings and how they should be lifted later. Keep the comments close to the
symbols that implement the shortcuts so they’re easy to find and update when the
parser is expanded.

Source: Coding guidelines

Comment thread zig/src/main.zig
Comment on lines +287 to +290
test "parsePrompt case-insensitive and trimmed (mirrors JS .trim().toLowerCase())" {
try expectSet("ultra", parseT(" /" ++ "PONYTAIL ULTRA ", "full"));
try expectSet("review", parseT("/PONYTAIL-Review", "full"));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep the case-insensitivity test tool-parametric.

This test hardcodes PONYTAIL, while the file otherwise uses TOOL; a Caveman build would no longer exercise the active tool name.

Proposed fix
 test "parsePrompt case-insensitive and trimmed (mirrors JS .trim().toLowerCase())" {
-    try expectSet("ultra", parseT("  /" ++ "PONYTAIL ULTRA  ", "full"));
-    try expectSet("review", parseT("/PONYTAIL-Review", "full"));
+    try expectSet("ultra", parseT("  /" ++ common.TOOL_UPPER ++ " ULTRA  ", "full"));
+    try expectSet("review", parseT("/" ++ common.TOOL_UPPER ++ "-Review", "full"));
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test "parsePrompt case-insensitive and trimmed (mirrors JS .trim().toLowerCase())" {
try expectSet("ultra", parseT(" /" ++ "PONYTAIL ULTRA ", "full"));
try expectSet("review", parseT("/PONYTAIL-Review", "full"));
}
test "parsePrompt case-insensitive and trimmed (mirrors JS .trim().toLowerCase())" {
try expectSet("ultra", parseT(" /" ++ common.TOOL_UPPER ++ " ULTRA ", "full"));
try expectSet("review", parseT("/" ++ common.TOOL_UPPER ++ "-Review", "full"));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@zig/src/main.zig` around lines 287 - 290, The parsePrompt casing/trim test is
hardcoded to PONYTAIL instead of using the existing TOOL constant, so it won’t
stay aligned with Caveman builds. Update the test in parsePrompt to build its
input from TOOL the same way the surrounding tests do, keeping the
case-insensitivity and trim coverage while exercising the active tool name.

@Eugene3dotdev
Eugene3dotdev merged commit 9d59544 into dev Jun 27, 2026
3 checks passed
Comment thread install.sh
{
"matcher": "startup|resume|clear|compact",
"hooks": [
{ "type": "command", "command": "$activate_bin", "timeout": 5 }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: Your installer writes timeout: 5 to settings.json while the hook manifests got upgraded to timeout: 30. This isn't a feature gap—it's a time bomb. Users who install via the shell script get 5-second timeouts while marketplace users get 30, creating a two-tier support nightmare.

🩹 The Fix:

Suggested change
{ "type": "command", "command": "$activate_bin", "timeout": 5 }
{ "type": "command", "command": "$activate_bin", "timeout": 30 }

📏 Severity: warning


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread install.sh
"UserPromptSubmit": [
{
"hooks": [
{ "type": "command", "command": "$hook_bin", "timeout": 5 }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: Same story as line 193—your installer still hands out 5-second timeouts when the plugin manifests upgraded to 30s. This is like leaving a “Beware of Dog” sign on a fence after the dog moved out.

🩹 The Fix:

Suggested change
{ "type": "command", "command": "$hook_bin", "timeout": 5 }
{ "type": "command", "command": "$hook_bin", "timeout": 30 }

📏 Severity: warning


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread install.sh
err " SessionStart → command: $HOOKS_DIR/ponytail-activate"
err " UserPromptSubmit → command: $HOOKS_DIR/ponytail-hook"
err " statusLine → command: $HOOKS_DIR/ponytail-statusline"
err " Or install via the plugin marketplace, which wires this for you."

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: When settings.json exists and isn’t ponytail-wired, you print helpful instructions but then cheerfully exit 0 with “Done!”. This is the software equivalent of a flight attendant demonstrating life vests on a plane that’s on fire.

🩹 The Fix: Return a non-zero exit code after the error messages so the installer truthfully reports failure.

📏 Severity: warning


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

function python() {
if (pythonCmd) return pythonCmd;
for (const cmd of ['python3', 'python']) {
try { execSync(`${cmd} -c "import sys"`, { stdio: 'pipe' }); pythonCmd = cmd; return pythonCmd; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: String-interpolated execSync with shell metacharacters. This is like handing a stranger your SSH key because they asked nicely.

🩹 The Fix: Replace execSync with execFileSync using argument arrays to avoid shell injection:

const { execFileSync } = require("node:child_process");
// ...
execFileSync(cmd, ['-c', 'import sys'], { stdio: 'pipe' });

📏 Severity: warning


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread zig/src/main.zig
}

test "parsePrompt case-insensitive and trimmed (mirrors JS .trim().toLowerCase())" {
try expectSet("ultra", parseT(" /" ++ "PONYTAIL ULTRA ", "full"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: The test hardcodes PONYTAIL instead of using common.TOOL_UPPER. When someone runs this with a Caveman build (TOOL="caveman"), the test will fail to catch uppercase commands. This is like testing a car's brakes but forgetting to turn the engine on.

🩹 The Fix:

Suggested change
try expectSet("ultra", parseT(" /" ++ "PONYTAIL ULTRA ", "full"));
try expectSet("ultra", parseT(" /" ++ common.TOOL_UPPER ++ " ULTRA ", "full"));
try expectSet("review", parseT("/" ++ common.TOOL_UPPER ++ "-Review", "full"));

📏 Severity: nitpick


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread tests/hooks.test.js
// spawn tests against hooks/ponytail-activate.js + hooks/ponytail-mode-tracker.js
// were removed with those files. What stays here is the pure-helper contract the
// surviving JS shims (pi / opencode / instructions bridge) still consume from
// hooks/ponytail-config.js.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: This file deletes half the tests and documents the intentional scope reduction, but doesn't wear the ponytail: badge of honor. How is anyone supposed to know this was deliberate and not just lazy?

🩹 The Fix: Add a ponytail: comment naming the ceiling (JS runtime coverage moved to Zig binaries) and upgrade path (revert if broader JS coverage needed).

📏 Severity: suggestion


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread bin/ponytail-launch.ps1
# .github/workflows/release-binaries.yml ships macOS + Linux only. Until a Windows
# archive ships there is no step-4 download here — if no binary resolves, this exits
# 0 (silent no-op) so a missing Windows binary never blocks SessionStart / prompt
# submission, exactly like the POSIX hooks' silent-fail contract.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: You've got paragraphs of explanation for why Windows is deferred, but they're not wearing the required ponytail: uniform. At least pretend you meant to do this.

🩹 The Fix: Collapse the multi-line comments into a single ponytail: marker beside the exit 0 fallback.

📏 Severity: suggestion


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

# exclude (ponytail has no claw test binary).
#
# Windows is deferred (R6-Windows: stdio/subprocess/argv libc → std.Io). The PS1
# shim covers Windows until then, so this workflow ships macOS + Linux only.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: Lines 12-13 explain why Windows is deferred (R6-Windows) but lack the required ponytail: marker format. The coding guidelines aren't suggestions—they're orders from the ponytail police.

🩹 The Fix: Prefix with ponytail: Windows release/download support is intentionally deferred... following the ceiling+upgrade-path format.

📏 Severity: suggestion


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jun 27, 2026 •

Copy link
Copy Markdown

Code Review Roast 🔥

Verdict: 8 Issues Found | Recommendation: Address before merge

Overview

Severity Count
⚠️ warning 3
💡 suggestion 4
🤏 nitpick 1
Issue Details (click to expand)
File Line Roast
install.sh 193 Installer writes 5s timeout while hook manifests use 30s
install.sh 200 Same timeout drift in UserPromptSubmit hook wiring
install.sh 268 Installer exits 0 after error instead of failing
benchmarks/robustness-audit.js 16 execSync command injection via string interpolation
zig/src/main.zig 288 Test hardcodes PONYTAIL instead of TOOL_UPPER
tests/hooks.test.js 10 Intentional JS coverage reduction needs ponytail: marker
bin/ponytail-launch.ps1 23 Windows-deferral comments need ponytail: format
.github/workflows/release-binaries.yml 13 Windows-deferral comments need ponytail: format

🏆 Best part: Finally killed the CVE-vulnerable SDK dependency and shipped pure Zig binaries. The symlink-safe flag writes and host detection dispatch are genuinely solid.

💀 Worst part: The install.sh timeout mismatch. Users who go through the shell installer get half the timeout allowance of marketplace users—classic “works on my machine” energy.

📊 Overall: Like a house flip that spent all the budget on the kitchen but forgot to match the outlet covers.

Fix these issues in Kilo Cloud

Files Reviewed (4 files)
  • install.sh - 3 issues (timeout drift ×2, exit code)
  • benchmarks/robustness-audit.js - 1 issue (command injection)
  • zig/src/main.zig - 1 issue (hardcoded tool name in test)
  • tests/hooks.test.js - 1 issue (missing ponytail: marker)
  • bin/ponytail-launch.ps1 - 1 issue (missing ponytail: marker)
  • .github/workflows/release-binaries.yml - 1 issue (missing ponytail: marker)

Reviewed by laguna-m.1-20260312:free · Input: 256.9K · Output: 15.9K · Cached: 2.8M

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants