Repository navigation
R6: pure-Zig native paths + dev↔main reconcile → main - #5
Conversation
* 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)
WHY: PR #1 was merged at 3d4f1f7 while CodeRabbit's final review (CHANGES_REQUESTED, 2026-06-19T20:49:16Z) still flagged a Major, still-valid issue that was never addressed in the merged commits: common.getDefaultMode declared an owned ([]u8) return but, on gpa.dupe failure, returned @constcast(DEFAULT_MODE) — a pointer into static rodata. All three callers (activate.zig, instructions.zig, mcp.zig) do `defer gpa.free(...)` on the result, so under allocator failure they would free non-heap memory: an invalid-free / UB path. WHAT: - getDefaultMode now returns FlagError![]u8 (FlagError already composes std.mem.Allocator.Error) and the fallback uses `return try gpa.dupe(u8, DEFAULT_MODE)` instead of @constcast. - Callers updated to preserve their existing failure semantics: activate.zig and instructions.zig use `catch return` (silent-fail hook contract); mcp.zig's resolveMode (already `![]u8`) uses `try`; the common.zig unit test uses `try`. IMPACT: zig/src/{common,activate,instructions,mcp}.zig. Eliminates the invalid-free path under OOM in all three Zig hook binaries while keeping the silent-fail contract intact. JS/Shell paths unaffected. VALIDATION: `zig build test -Dtool=ponytail` -> 52/52 tests pass, exit 0. `zig build -Dtool=ponytail` (full) exit 0. `zig fmt --check` on the four changed files exit 0. (caveman build fails pre-existingly because skills/caveman/SKILL.md is absent in this repo — verified identical failure on the pristine merged tree; unrelated to this change. ziglint not installed in env.)
…turn fix(zig): make getDefaultMode honor owned-return contract (PR #1 follow-up)
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)
…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
feat(zig): R6 — reconcile dev↔main, ship Zig binaries, retire independent JS
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
📝 WalkthroughSummary by CodeRabbit
WalkthroughAdds a release workflow for version-tagged binaries, new Bash and PowerShell launchers, installer shims, hook manifest rewiring, and Zig runtime changes for fallible default-mode lookup and expanded prompt parsing. ChangesPure-Zig runtime cutover
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 13
🤖 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 62-64: The cross-compile step in the release workflow expands
matrix.target.triple directly inside the run shell, which static analysis flags
as template-injection; move that value into the step’s env for consistency with
the RAW_TAG hardening and update the nix develop/zig build command to reference
the environment variable instead. Use the existing “Cross-compile ponytail for
…” job step and the matrix.target.triple symbol to locate the change.
- Around line 12-13: Add the required ponytail marker to the deferred-Windows
note in the release-binaries workflow comment so the intentional macOS +
Linux-only simplification is discoverable by convention. Update the existing
explanatory comment near the Windows deferral to start with ponytail: and
include the known ceiling plus upgrade path, keeping the same meaning but making
the marker explicit.
- Around line 44-53: Resolve and validate the release tag before any repository
checkout, then use that validated tag as the ref for both actions/checkout
steps. In the release workflow, update the checkout logic around the Resolve and
validate release tag step so the build always uses the same tag being published,
preventing a manual run from compiling the triggering branch while labeling
artifacts with inputs.tag. Ensure the tag value flows only from the validated
tag output into checkout, release asset creation, and checksum generation.
- Line 44: The workflow still uses floating action tags and leaves checkout
credentials persisted, so update both checkout steps in the release-binaries
workflow to disable credential persistence via the checkout action settings, and
replace the current tagged uses of actions/checkout and
softprops/action-gh-release with pinned full commit SHAs while keeping the
human-readable version in a comment. Make sure the changes are applied
consistently wherever those actions are referenced so the build and release jobs
stay reproducible and do not reuse the token from .git/config.
- Around line 15-18: The release workflow trigger currently allows overlapping
runs for the same version, which can race release creation and asset uploads.
Add a concurrency guard to the workflow using a tag/version-derived group so
runs for the same release serialize or cancel, and place it alongside the
existing on/push/tags and workflow_dispatch triggers in the release workflow
definition.
In `@bin/ponytail-launch.ps1`:
- Around line 19-23: Add a ponytail: comment in the Windows no-download/no-op
branch of ponytail-launch.ps1, near the logic that exits 0 when no binary
resolves. Use the existing local/cache resolution flow and explicitly note the
known ceiling (no Windows archive yet, silent no-op) plus the upgrade path once
the Windows binary ships, so the intentional simplification is documented.
In `@install.ps1`:
- Around line 8-13: The Windows no-op installer in the install script is an
intentional simplification and should be marked with a ponytail: comment. Update
the comment near the Windows install path handling to explicitly note the
current ceiling (accepts flags and exits 0 without installing) and the upgrade
path (plugin marketplace/bin/ponytail-launch until R6-Windows ships), using the
existing install flow symbols so the intent is clear.
In `@install.sh`:
- Around line 177-181: Add the required ponytail: marker to the settings-writer
shortcut comments in install.sh, including the fresh-file-only settings.json
wiring and the already_installed() grep-based idempotency probe. Update the
existing explanatory comments near the settings writer logic to explicitly name
the intentional ceiling and the upgrade path, keeping the reference points in
the same shell block that handles settings.json wiring and idempotency checking.
- Around line 158-175: The symlink checks in safe_install_file only validate the
destination and immediate parent before mv -f, so the install can still be raced
by swapping an ancestor path after the pre-check. Tighten the implementation to
make the final write atomic and revalidated at the last possible moment, using
safe path handling around safe_install_file’s mktemp/cp/chmod/mv sequence so the
destination cannot be redirected outside the intended hooks directory.
- Around line 257-279: In the install flow in install.sh, the branch that
handles a non-empty settings.json not already handled by already_installed only
prints manual wiring instructions but then falls through to the success
messages. Update that path to stop the install as a failure (or return early
before the final “Done!”/“What's installed” output) so the script does not
report success when wire_settings_fresh was not applied. Use the existing
already_installed and wire_settings_fresh decision block to keep the success
message only for actually wired installs.
- Around line 182-207: The fresh settings writer in wire_settings_fresh() is
emitting unquoted hook command paths into settings.json, which breaks installs
when HOOKS_DIR or related base paths contain spaces. Update the command values
for ponytail-activate, ponytail-hook, and ponytail-statusline so they are quoted
consistently with the plugin manifests before being written, ensuring the
generated JSON preserves each full path as a single shell-parsed command string.
In `@tests/hooks-windows.test.js`:
- Around line 56-71: The windows hook test currently only verifies
bin/ponytail-launch exists, so it can miss a missing PowerShell launcher
artifact. Update the existing test in commandWindows/allCommands to also assert
that bin/ponytail-launch.ps1 exists on disk, alongside the current resolver
check, so packaging regressions for Windows hooks are caught.
In `@tests/hooks.test.js`:
- Around line 2-32: The current tests only cover the helper contract in
ponytail-config via isShellSafe and no longer exercise the launcher/install
flow. Add back at least one smoke test that covers the new executable path for
bin/ponytail-launch and install.sh, including the manifest-to-binary wiring, so
regressions in the shell layer are caught. Keep the existing isShellSafe checks
in tests/hooks.test.js, but supplement them with a real end-to-end invocation
tied to the launcher/install entrypoints.
🪄 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: e628f7d8-b6b2-4487-8d34-6a40ad46ca55
📒 Files selected for processing (22)
.github/workflows/release-binaries.ymlbin/ponytail-launchbin/ponytail-launch.ps1hooks/claude-codex-hooks.jsonhooks/copilot-hooks.jsonhooks/ponytail-activate.jshooks/ponytail-mode-tracker.jshooks/ponytail-runtime.jsinstall.ps1install.shponytail-mcp/README.mdponytail-mcp/index.jsponytail-mcp/instructions.jsponytail-mcp/package.jsonponytail-mcp/test/instructions.test.jstests/hooks-windows.test.jstests/hooks.test.jszig/src/activate.zigzig/src/common.zigzig/src/instructions.zigzig/src/main.zigzig/src/mcp.zig
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
EugOT/dotfiles(manual) → reviewed against branchdevinstead of the default branch
💤 Files with no reviewable changes (8)
- ponytail-mcp/test/instructions.test.js
- hooks/ponytail-activate.js
- ponytail-mcp/package.json
- hooks/ponytail-mode-tracker.js
- ponytail-mcp/README.md
- ponytail-mcp/index.js
- ponytail-mcp/instructions.js
- hooks/ponytail-runtime.js
WHY: 13 CodeRabbit findings on the dev→main PR, all verified real against current code. WHAT: release-binaries.yml: matrix.target.triple → step env $TRIPLE (no shell injection); pin actions/checkout + softprops/action-gh-release to commit SHAs + persist-credentials false on both checkouts; validate the v<semver> tag in its own job, flow output into both checkout ref: (dispatch builds the published tag); tag-keyed concurrency group; ponytail: marker on the Windows-deferral comment. install.sh: harden safe_install_file vs ancestor-swap TOCTOU (refuse symlinked dst/parent, resolve parent realpath, stage tmp inside, last-moment re-validate, atomic rename); non-wired-settings branch returns 1 before success output; wire_settings_fresh shell-quotes hook command paths; ponytail: markers on settings/idempotency blocks. install.ps1 + bin/ponytail-launch.ps1: ponytail: markers on Windows no-op paths. tests: hooks-windows asserts ponytail-launch.ps1 exists; hooks.test adds a launcher/ install wiring smoke test. IMPACT: CI security + install correctness; no Zig runtime change. VALIDATION (clean cache, zig-0.16.0-dev.3142): - zig build + zig build test exit 0; YAML/JSON valid; bash -n OK; biome clean - sandboxed install.sh: dry-run rc=0; fresh-wire rc=0 + quoted-path valid settings.json; non-wired → rc=1 (no Done!); symlinked hooks dir → rc=1, 0 binaries leaked - bun test both hook files: pass
fix(install): R6.6 PR#5 review fixes (release workflow + install.sh hardening)
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@tests/hooks.test.js`:
- Around line 89-95: The wiring assertion in the hook install test is too broad
because `installSh.includes(name)` can be satisfied by the hook list declaration
instead of the settings-writing logic. Update the test around
`wire_settings_fresh` to extract or target the settings-writer body in
`installSh`, and assert that it references `$HOOKS_DIR/<name>` for each deployed
hook. Use the existing `deployed` loop and the `wire_settings_fresh` symbol to
keep the check scoped to the code that writes `settings.json`.
🪄 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: 9f4a03c7-e7ac-4c29-8187-9de645c39485
📒 Files selected for processing (6)
.github/workflows/release-binaries.ymlbin/ponytail-launch.ps1install.ps1install.shtests/hooks-windows.test.jstests/hooks.test.js
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
EugOT/dotfiles(manual) → reviewed against branchdevinstead of the default branch
| // install.sh's wire_settings_fresh must reference each deployed hook so the | ||
| // settings.json it writes actually points at the binaries it lays down. | ||
| for (const name of deployed) { | ||
| assert.ok( | ||
| installSh.includes(name), | ||
| `install.sh deploys '${name}' but never references it in settings wiring`, | ||
| ); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Scope the wiring assertion to wire_settings_fresh.
installSh.includes(name) is tautological because each name came from HOOK_BINS=(...) in the same file. Extract the settings-writer body and assert it references $HOOKS_DIR/<name>.
Proposed fix
// install.sh's wire_settings_fresh must reference each deployed hook so the
// settings.json it writes actually points at the binaries it lays down.
+ const wiringMatch = installSh.match(/wire_settings_fresh\(\) \{([\s\S]*?)\n\}/);
+ assert.ok(wiringMatch, "install.sh must define wire_settings_fresh()");
+ const wiring = wiringMatch[1];
for (const name of deployed) {
assert.ok(
- installSh.includes(name),
+ wiring.includes(`$HOOKS_DIR/${name}`),
`install.sh deploys '${name}' but never references it in settings wiring`,
);
}📝 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.
| // install.sh's wire_settings_fresh must reference each deployed hook so the | |
| // settings.json it writes actually points at the binaries it lays down. | |
| for (const name of deployed) { | |
| assert.ok( | |
| installSh.includes(name), | |
| `install.sh deploys '${name}' but never references it in settings wiring`, | |
| ); | |
| // install.sh's wire_settings_fresh must reference each deployed hook so the | |
| // settings.json it writes actually points at the binaries it lays down. | |
| const wiringMatch = installSh.match(/wire_settings_fresh\(\) \{([\s\S]*?)\n\}/); | |
| assert.ok(wiringMatch, "install.sh must define wire_settings_fresh()"); | |
| const wiring = wiringMatch[1]; | |
| for (const name of deployed) { | |
| assert.ok( | |
| wiring.includes(`$HOOKS_DIR/${name}`), | |
| `install.sh deploys '${name}' but never references it in settings wiring`, | |
| ); |
🤖 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 89 - 95, The wiring assertion in the hook
install test is too broad because `installSh.includes(name)` can be satisfied by
the hook list declaration instead of the settings-writing logic. Update the test
around `wire_settings_fresh` to extract or target the settings-writer body in
`installSh`, and assert that it references `$HOOKS_DIR/<name>` for each deployed
hook. Use the existing `deployed` loop and the `wire_settings_fresh` symbol to
keep the check scoped to the code that writes `settings.json`.
Lands ponytail's R6 on main: dev↔main reconcile (absorbs main's CVE fix + upstream merge) + pure-Zig native install paths.
What changed
bin/ponytail-launch ponytail-{activate,hook}. main.zig command surface ports the full retired JS syntax (@Ponytail, /ponytail-review, off, namespaced, stop ponytail).Honest scope
The pi-extension chain stays JS (pi mandates ESM): config→fs-safe→instructions→instructions-bin. Native paths are pure Zig; pi runtime keeps its JS shim layer.
Review
P1 command-parity + P2 Windows commandWindows fixed + verified (runtime: every command form maps correctly; commandWindows → ponytail-launch.ps1). CodeRabbit Majors on release-binaries.yml applied (perms scoped, tag validated, binary-set asserted, no empty SHA256SUMS). build+test+fmt exit 0.
Deferred
Windows binaries = R6-Windows. benchmarks/robustness-audit.js execSync (CodeRabbit Minor, merged-from-main benchmark tool) — out of R6 scope.
🤖 Generated with Claude Code