Skip to content

fix: keep path identity when validators accept macOS path aliases - #240

Open
zengchang233 wants to merge 1 commit into
PolyArch:devfrom
zengchang233:fix/validator-path-identity
Open

zengchang233 wants to merge 1 commit into
PolyArch:devfrom
zengchang233:fix/validator-path-identity

Conversation

@zengchang233

Copy link
Copy Markdown

Summary

aeaec5c (from #141) made the loop validators accept both /var/... and /private/var/... spellings of the active loop directory on macOS. Two of those changes go further than intended: one lets the Bash validator accept paths outside the loop, and the other makes the Write/Edit validators reject a valid goal-tracker path. This PR keeps the macOS tolerance and fixes both problems.

I found these while applying the aeaec5c hook fixes to a local 1.16.0 install for the macOS sed stall described in #238.

Problems

1. The Bash validator's todos check matches a substring

hooks/loop-bash-validator.sh strips a leading /private from the active loop directory, then matches the rest anywhere in the command. So a command that writes round-1-todos.md is accepted in two cases where it should not be:

  • Another directory whose path ends with the loop path. With the loop at /tmp/p/.humanize/rlcr/s, writing to /tmp/other/tmp/p/.humanize/rlcr/s/round-1-todos.md passes.
  • A separate /tmp on Linux. There, /private/tmp and /tmp can be unrelated directories. A loop under /private/tmp/p also authorizes /tmp/p.

2. The Write/Edit validators' goal-tracker check has no lexical fallback

hooks/loop-write-validator.sh and hooks/loop-edit-validator.sh use _normalize_path only when canonicalize_path fails. But when neither realpath nor python3 is available, canonicalize_path returns the raw path with status 0. The lexical fallback therefore never runs, and <loop>/./goal-tracker.md or <loop>//goal-tracker.md is rejected.

Fix

  • Bash validator. Resolve both ${ACTIVE_LOOP_DIR#/private} and /private${ACTIVE_LOOP_DIR#/private} with canonicalize_path. Accept (/private)?<path> only when both resolve to the same existing directory; otherwise require the exact active loop path, as before aeaec5c. The match must also start at a path boundary, so a directory that merely ends with the loop path is not accepted.
  • Write/Edit validators. Always pass the canonicalizer's result through _normalize_path.

Tests

New cases in tests/test-allowlist-validators.sh:

Test Validator Case Expected
40 Bash canonical (/private) spelling of the loop dir allowed
41 Bash directory ending with the loop path blocked
42 Bash directory containing the canonical loop path blocked
43 Write <loop>/./goal-tracker.md with realpath and python3 unavailable allowed
44 Edit <loop>//goal-tracker.md with realpath and python3 unavailable allowed
45 Bash other /private spelling where it is not an alias (realpath stub that leaves paths unchanged) blocked
46 Bash exact loop path in the same setup allowed

Results:

  • Current dev hooks: 49 pass, 5 fail (41–45).
  • This branch: 54/54 pass.

Verification

  • Full suite, file by file. Run on macOS with /bin/bash 3.2 (run-all-tests.sh needs bash 4), on dev and on this branch, from the same location. The same files fail on both, with identical FAIL lines. There are no new failures.
  • Location-dependent cancel test. Unrelated to this change: tests/test-cancel-signal-file.sh fails 50 of 68 cases on both dev and this branch when the checkout lives under /private/tmp. It passes 68/68 from a checkout under $HOME. I have not looked into it here.
  • Review. The change went through Humanize's own RLCR review-only loop (codex review --base dev), which reported no findings.

Related: #238

🤖 Generated with Claude Code

aeaec5c made the validators tolerate /var vs /private/var on macOS, but two
of those changes widen or break the checks:

- The Bash validator strips a leading /private from the active loop dir and
  then matches it as a substring of the command. A command writing
  round-1-todos.md under another directory that merely ends with the loop
  path, such as /tmp/other/tmp/p/.humanize/rlcr/s, passes; on Linux, where
  /private/tmp and /tmp can be unrelated, a loop under /private/tmp/p also
  authorizes /tmp/p. Accept the other spelling only when canonicalize_path
  resolves both to the same existing directory, and require the path to
  start at a word boundary. Otherwise the exact loop path is required, as
  before.
- The Write and Edit validators fall back to _normalize_path only when
  canonicalize_path fails, but it returns the raw path with status 0 when
  neither realpath nor python3 is available, so <loop>/./goal-tracker.md
  is rejected. Normalize the canonicalizer's result as well.

Regression tests cover both spellings of the loop path, suffix and nested
directories, a platform without the alias (realpath that does not resolve
/private) and the tracker paths with realpath and python3 unavailable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant