Skip to content

feat(install): install Cursor tool-dedupe hooks - #1213

Open
minhhung2556 wants to merge 19 commits into
JuliusBrussee:mainfrom
minhhung2556:cursor/hook-on-install
Open

minhhung2556 wants to merge 19 commits into
JuliusBrussee:mainfrom
minhhung2556:cursor/hook-on-install

Conversation

@minhhung2556

@minhhung2556 minhhung2556 commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Install writes a Cursor hook that drops repeated unchanged Read and shell calls, and uninstall removes it.
  • Windows npx.cmd shims that set NPX_CLI_JS now resolve, so portable installs can spawn skills.
  • The hook installer no longer requires src/hooks at load time, so a detached bin/ package can still install other providers.

Test plan

  • node --test tests/installer/cursor-hooks.test.mjs tests/installer/cursor-dedupe-tools.test.mjs tests/installer/portable-process.test.mjs tests/installer/provider-skills-integration.test.mjs
  • CI installer suite on the pull request

@minhhung2556
minhhung2556 marked this pull request as draft October 7, 2026 08:51
@minhhung2556

Copy link
Copy Markdown
Contributor Author

Cursor dedupe-hook token measurement

node --test tests/installer/cursor-dedupe-tools.test.mjs: 15/15 pass.

A denied repeat replaces the tool payload with a short agent_message (~89–132 characters, ~23–33 tokens). Savings = payload chars that would re-enter context minus that message. Token estimate is ceil(chars / 4).

Repeat call Payload Deny message Saved tokens
src/hooks/cursor-dedupe-tools.js 12,799 chars / 3,200 tok 114 / 29 3,171
bin/lib/cursor-hooks.js 4,445 / 1,112 114 / 29 1,083
CLAUDE.md 40,012 / 10,003 114 / 29 9,974
packages/cli/src/index.ts (20,177 lines) 927,202 / 231,801 114 / 29 231,772
tests/installer/cursor-dedupe-tools.test.mjs 11,339 / 2,835 114 / 29 2,806
index.ts lines 1–80 (already covered by the full read) 3,308 / 827 132 / 33 794
Grep decideRead in src/hooks 219 / 55 112 / 28 27
Grep cursor-dedupe in the repo 695 / 174 112 / 28 146
Glob tests/**/*.test.mjs 1,885 / 472 112 / 28 444
git status 641 / 161 89 / 23 138

10 denied repeats: 1,001,418 characters, about 250,355 tokens.

Without the full index.ts read, the other nine repeats still save 74,330 characters, about 18,583 tokens. One reread of CLAUDE.md is about 9,974 tokens. Grep, Glob, and git status save less because those outputs are already small.

minhhung2556 and others added 6 commits October 8, 2026 12:52
Node 20+ npx.cmd sets NPX_CLI_JS instead of a node line the portable resolver already understood, so Windows installs failed to spawn skills.

Co-authored-by: Cursor <cursoragent@cursor.com>
Repeated unchanged Read and shell calls stay in context. Install writes the hook under ~/.cursor; uninstall removes it.

Co-authored-by: Cursor <cursoragent@cursor.com>
Detached installs ship bin/ alone. Requiring the hook script from that package made every provider install fail before Cursor was even selected.

Signed-off-by: Hung Luong Do Minh <minhhung2556@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Deny repeat tool calls in the same chat when the tree or file is unchanged. Register Grep and Glob preToolUse matchers and cap hook state by conversation count.

Co-authored-by: Cursor <cursoragent@cursor.com>
Detached installs have no checkout to copy the script from. A real copy fails Cursor install on that path.

Co-authored-by: Cursor <cursoragent@cursor.com>
Windows can keep the same mtime after an append, so an unchanged mtime still denied a new Grep.

Co-authored-by: Cursor <cursoragent@cursor.com>
@minhhung2556

Copy link
Copy Markdown
Contributor Author

Local reinstall + output-token A/B (Cursor)

Ran from a full clone on Windows after node installer/install.js --only cursor --non-interactive --force (exit 0).

Install observed

  • 22 skills via npx skills add … -a cursor -g → %USERPROFILE%\.agents\skills\ (pinned GitHub ref v3.2.0, not a local path override)
  • Owned from clone repoRoot: %USERPROFILE%\.cursor\agents\cavecrew-*.md, %USERPROFILE%\.cursor\caveman\ (session hook payload), hooks.json sessionStart → caveman-host-session-start.js cursor
  • Installer still logged dedupe hook as pilot (“would copy …”); existing hooks.json already had preToolUse / beforeShellExecution entries for cursor-dedupe-tools.js on this machine

Three read-only tasks, two arms (same prompts against this repo; no edits)

  1. What does --only cursor install and which files does it touch?
  2. Where does the Cursor session hook live, and what does it do?
  3. What does docs/HONEST-NUMBERS.md say is / is not a valid savings claim?
Arm Method
Baseline Subagent per task, full prose, no caveman skill
Caveman Terse answers, facts kept (caveman voice)

Output tokens (tiktoken o200k_base, approximate output length only — not provider billing):

Task Baseline Caveman Saved
1 1,901 217 88.6%
2 1,349 161 88.1%
3 1,077 156 85.5%
Median 88.1%

saved% = (baseline − caveman) / baseline

Caveats (per HONEST-NUMBERS): input, cache, and rule injection are excluded; this is not an invoice %. Complements the dedupe-hook table in the PR comment (tool payload not re-entering context) — different layer (response style vs repeated tool output).

Happy to re-run with this branch’s installer once dedupe install is no longer pilot-only, or with provider-billed A/B on the same tasks.

@minhhung2556
minhhung2556 force-pushed the cursor/hook-on-install branch from db18a97 to ccccd9e Compare October 8, 2026 06:38
minhhung2556 and others added 3 commits October 8, 2026 13:52
Uninstall always named .cursor/hooks.json, so a Codex uninstall that never wrote hooks failed doesNotMatch. Also skip the pilot after a failed skills add and on --no-hooks.

Signed-off-by: Hung Luong Do Minh <minhhung2556@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Hung Luong Do Minh <minhhung2556@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
CI has no user.name, so the temp commit never landed and the
fingerprint stayed empty. The repeat status was allowed.

Co-authored-by: Cursor <cursoragent@cursor.com>

Copy link
Copy Markdown
Owner

Reviewed now that it's out of draft. Needs changes — not adopted. Thanks for the volume of test coverage here; the review below is about two separable halves.

8d3440b (npm npx.cmd shims) — the fix is already in main, and the new branch loosens a deliberate guard. parseWindowsNodeShim has handled SET "NPX_CLI_JS=..." since the "Node.js npm npx.cmd variable form" test; I checked out your branch's test file against unmodified main and all 9 tests pass, including yours. The existing branch requires the target to end in .js and the shim to actually launch "%NODE_EXE%" "%NPX_CLI_JS%" %* ("arbitrary variable expansion stays rejected"). Your regex runs first and returns immediately, so it bypasses both. Comparing the two on the same inputs:

shim source main this PR
stock npm npx.cmd accepts accepts
assignment present, never used to launch rejects accepts
non-.js target (payload.bat) rejects accepts
..\..\..\Users\Public\x.js rejects accepts

Downstream resolveWindowsNodeShim still requires the target to exist, so this isn't code execution — but it accepts three shapes the resolver was written to refuse, for no behavior gain. If you did hit a real Windows shim main can't parse, please paste that .cmd verbatim; that's a failing case worth a test.

The dedupe hook — four repo invariants block it as written (CLAUDE.md):

  1. src/hooks/cursor-dedupe-tools.js is new under src/hooks/, so src/hooks/checksums.sha256 must be regenerated or tests/verify_repo.py fails the build.
  2. saveState does fs.writeFileSync on ~/.caveman/cursor-dedupe-tools-state.json. Every flag-file write must go through safeWriteFlag() — a predictable user-owned path is the symlink-clobber surface that helper exists for.
  3. main() reads stdin with for await (const chunk of process.stdin), i.e. to EOF. Hook readers must return on the first complete JSON object (UserPromptSubmit hook waits for stdin EOF, so Claude Code kills it at the 5s timeout — 2.3% of runs inject nothing #729/fix(hooks): Windows stdin EOF stall silently kills the UserPromptSubmit hook #833/Codex native integration: wrap daemon never spawns on Windows 'codex exec' - infinite reconnect on /w/codex/responses #949 — Windows pipe close lags arbitrarily) and carry a keep-the-writer-open regression test.
  4. installCursorHooks prints a plan and writes nothing, while install.js already calls installCursorNative(ctx) for the same provider — so it ships an installer no-op next to a live Cursor hook path, and CLAUDE.md warns that two Cursor hooks inject twice.

Beyond the invariants, a hook that returns permission: 'deny' to Read/Grep/Glob/npm test is a product-behavior decision for @JuliusBrussee, not something this routine adopts. Worth splitting: the invariants are mechanical, the deny policy is a maintainer call.


Generated by Claude Code

minhhung2556 and others added 8 commits October 9, 2026 14:59
The early NPX_CLI_JS match returned before the launch-line and .js checks, so unused assignments and non-js targets were accepted.

Co-authored-by: Cursor <cursoragent@cursor.com>
A direct writeFileSync on ~/.caveman/cursor-dedupe-tools-state.json is the symlink-clobber path that helper exists to refuse.

Co-authored-by: Cursor <cursoragent@cursor.com>
An EOF read waits out a lagging Windows pipe close after the payload is already complete.

Co-authored-by: Cursor <cursoragent@cursor.com>
verify_repo fails the build when a new src/hooks file is missing from the closed allowlist.

Co-authored-by: Cursor <cursoragent@cursor.com>
The pilot installer only printed a plan beside the live Cursor hooks.json writer, so two hook paths could inject twice.

Co-authored-by: Cursor <cursoragent@cursor.com>
checksums.sha256 pins cursor-dedupe-tools.js for Cursor/Copilot
payload; Claude detached install never copies it.

Refs JuliusBrussee#1213

Co-authored-by: Cursor <cursoragent@cursor.com>
A permanent deny on the same full Read traps the agent. One deny, then allow. Drop offset/limit from the deny text.

Co-authored-by: Cursor <cursoragent@cursor.com>
Mixed session checks fresh calls stay allowed. Repo table subtracts the deny message from each repeated payload.

Co-authored-by: Cursor <cursoragent@cursor.com>
@minhhung2556

Copy link
Copy Markdown
Contributor Author

Cursor dedupe-hook token measurement

node --test tests/installer/cursor-dedupe-tools.test.mjs: 18/18 pass.

A denied repeat replaces the tool payload with a short agent_message. Savings equal the payload that would re-enter context minus that message. The token estimate is ceil(chars / 4). The saved percentage is saved tokens / payload tokens.

Repeat call Payload Deny message Saved tokens Saved %
src/hooks/cursor-dedupe-tools.js 14,094 chars / 3,524 tok 75 / 19 3,505 99.5%
installer/lib/cursor-dedupe-hooks.js 2,576 chars / 644 tok 75 / 19 625 97.0%
CLAUDE.md 47,166 chars / 11,792 tok 75 / 19 11,773 99.8%
packages/cli/src/index.ts (full file) 941,797 chars / 235,450 tok 75 / 19 235,431 100.0%
tests/installer/cursor-dedupe-tools.test.mjs 31,272 chars / 7,818 tok 75 / 19 7,799 99.8%
index.ts lines 1–80 (already covered by the full read) 3,308 chars / 827 tok 132 / 33 794 96.0%
Grep decideRead in src/hooks 176 chars / 44 tok 112 / 28 16 36.4%
Grep cursor-dedupe in the repo 295 chars / 74 tok 112 / 28 46 62.2%
Glob tests/**/*.test.mjs 2,242 chars / 561 tok 112 / 28 533 95.0%
git status 385 chars / 97 tok 89 / 23 74 76.3%

10 denied repeats: 1,043,311 characters, about 260,831 tokens without dedupe, about 235 tokens with dedupe (deny messages only), saved 260,596 tokens (100%).

Without the full packages/cli/src/index.ts read, the other 9 repeats still save 101,514 characters, about 25,165 tokens. The Grep rows stay low because the deny message is close to the payload size.

minhhung2556 and others added 2 commits October 9, 2026 18:01
Hook edit in ae24619 left verify_repo failing on macos and root-surfaces.

Refs JuliusBrussee#1213

Co-authored-by: Cursor <cursoragent@cursor.com>

This branch has not been deployed

No deployments
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.

2 participants