Gemini CLI runs every hook command through `bash -c <command>`, and
Cursor's hook documentation requires script hooks to be executable. The
four Gemini hooks and the Cursor pre-tool-use, post-tool-use and stop
hooks were committed as 100644, so in a macOS or Linux clone they failed
with "Permission denied" and did nothing. Mark every .sh under
.cursor/hooks and .gemini/hooks executable, and add a test that checks
the git mode of each script the two manifests run by path.
Cursor submits a stop hook's followup_message as the next user message.
When every phase of the plan was complete, stop.sh and stop.ps1 still
returned an "ALL PHASES COMPLETE" followup_message, so each stop in a
project with a finished plan triggered automatic follow-up turns, up to
the manifest's loop_limit of 3. This was live on the Windows route and
would reach macOS and Linux once the shell hooks are executable. Exit
without output instead, as completed plans do on the other routes; an
incomplete plan still asks Cursor to continue. Both branches are tested
under sh, Windows PowerShell and pwsh.
The cleanup after replacement returned failure on the first failed
inspection. With concurrent selectors the pointer is briefly missing
while another writer replaces it, so a call whose own Replace had
already succeeded exited 1 and left its .replace-backup behind: about 1
in 10 calls in an 8-writer stress run on Windows PowerShell 5.1 and
pwsh 7. Because the pointer was already switched, init-session.ps1 then
skipped the gated-mode files for a plan that was active.
Retry transient inspection failures instead of giving up. After a
successful Replace, only delete the owned backup: it holds the
superseded value, so it is never moved back and never gated on the
current pointer, and failing to delete it is a warning, not an error.
The regression test holds the backup open after a successful Replace
and expects exit 0, the new pointer and the warning.
test_stale_pointer_falls_through_to_the_root_plan still expected the
plan text on preToolUse output, which now carries only the permission
answer. It failed on ubuntu and macOS CI and on Windows. Assert the root
plan in the sessionStart additional_context and the allow response from
preToolUse instead.
session-start.sh called python3 without -I, so a json.py in the project
directory was imported and executed when a Cursor session started. On
Windows the Microsoft Store python3 alias also made the hook return {}.
Try python3, then python, each with -I -X utf8, and keep the {} response
when neither runs. Cover the isolation with a planted json.py test and a
check that every interpreter call in the hook is isolated; the
failing-Python test now stubs both names.
The Gemini skill metadata still named SessionStart, BeforeTool, AfterTool
and BeforeModel after the hooks moved to BeforeAgent and SessionEnd.
List the events .gemini/settings.json registers, and assert in the Gemini
contract test that the metadata line and the settings keys agree.
The Stop hook runs check-complete --gate on every turn end. In a cwd with
no task_plan.md it echoed "No task_plan.md found — no active planning
session.", which the Claude Code hook surfaced as a system notice after
every reply in every session that never opted into planning.
The script header already promises that plans stay silent in --gate mode;
this makes the no-plan branch honor it. The explicit report (no --gate,
/status) keeps the notice. Same change in check-complete.ps1, the five
language variants, and the synced IDE copies; regression test in
tests/test_quiet_completion_hooks.py covers both checkers and all three
hook routes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
PowerShell route on OneDrive (#275: pointer checks and the attester refuse only symlinks, junctions and name-surrogate reparse points) and the session-catchup copy guard checks tracked copies only (#274). Parity set bumped with scripts/bump-version.py; CHANGELOG, README releases row.
ValidateRegular in the attester's native helper threw "Refusing a
reparse-point file" for any file with the ReparsePoint attribute, so in a
project under OneDrive (Files On-Demand marks every synced file, tag
0x9000601A) attestation, --show and --clear all failed, and init-session.ps1
still printed "attested" because it swallows the attester's failure. The
check now refuses only name-surrogate tags (bit 29 set: symlinks, junctions
and any unknown surrogate), which is exactly the set Windows path parsing
follows, so the security property is unchanged and placeholder files read
as the regular files they are. Every mirror synced (#275).
resolve-plan-dir.ps1, the Cursor hook resolve-plan-context.ps1 and the
set-active-plan.ps1 writer refused an .active_plan pointer that carried the
ReparsePoint attribute. OneDrive Files On-Demand marks every synced file as
a reparse point, so in a project under OneDrive the Cursor hooks stopped
with the unsafe-pointer notice, the resolver printed nothing for
attest-plan.ps1 and check-complete.ps1, and set-active-plan.ps1 refused to
write, while every other route read the pointer normally. The three checks
now refuse a container or a LinkType of SymbolicLink or Junction, the same
predicate v3.20.3 gave the directory checks and the `[ -L ]` of the shell
scripts. Every mirror synced. Tests cover the plain pointer and the
symlinked pointer (skipped where file symlinks need a privilege). Fixes#275.
The exact-basename guard walked the whole checkout, so any untracked copy
of session-catchup.py, such as a stale clone kept under the gitignored
.planning/ directory on a maintainer machine, was loaded as a shipped copy
and failed the two tests with 120 subtests while CI stayed green. Shipped
means tracked: the copies now come from git ls-files, with the filtered
walk kept only for a checkout without git, where .planning and .git are
excluded as well. Fixes#274.
A symlinked or junctioned directory under .planning/ is never a plan on
any route (#270, PR #271 by @ShaunLinTW plus three maintainer commits).
Parity set bumped to 3.20.3 with scripts/bump-version.py. CHANGELOG,
CONTRIBUTORS.md (PR #271 for Shaun Lin), README releases row.
Review of the previous two commits found the PowerShell resolver testing the
ReparsePoint attribute, which every OneDrive Files On-Demand directory
carries: a project under OneDrive would have lost every plan on the Cursor
route. The resolver now tests LinkType for SymbolicLink or Junction, the
exact `[ -L ]` of the shell scripts and is_link of the Python twin, and the
Codex counter uses the same symlink-or-junction tag rule instead of the
adapter's any-reparse helper.
set-active-plan.sh and set-active-plan.ps1 refuse to point the shared
default at a linked directory and leave it out of --list, since no route
selects it; the sh/py/ps1 suites cover PLAN_ID naming a link (refused with
the existing notice) and the pointer naming one (falls through to the real
plan like a stale pointer). Hermes docstring names the routes precisely (#270).
The Codex adapter's session_plan_requires_binding and the shared
countSelectablePlans of the OpenCode and DeepSeek Harness plugins counted a
symlinked or junctioned plan directory although their selection paths (and
the shell family since the previous commit) refuse it. A linked directory
never counts now, so the several-plans notice fires on the same trees on
every route. The two TypeScript cores stay byte-identical (#270).