Skip to content

internal: Give Claude Code the same agent rules, skills and hooks as Cursor - #4207

Merged
ntucker merged 26 commits into
masterfrom
claude/project-thread-70f0pv
Oct 5, 2026
Merged

ntucker merged 26 commits into
masterfrom
claude/project-thread-70f0pv

Conversation

@ntucker

@ntucker ntucker commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

Requested by Nathaniel · project thread

Motivation

Claude Code ignores .cursor/rules/*.mdc and .agents/skills, so Claude sessions only got AGENTS.md and the pre-push hook. Agents had to read rules by hand, and skills were invisible.

Solution

Cursor's files stay the single source. Claude Code's copies are generated or linked, and CI fails when they drift.

Cursor Claude Code
<dir>/.cursor/rules/*.mdc (globs) <dir>/.claude/rules/*.md (paths), generated by yarn build:agent-rules. They sit at the same depth, so relative links resolve and nested rules (website/blog) still load only for files in their folder
alwaysApply: true rule without paths (none today)
description-only rule (interface-design) no rule equivalent. Moved to .agents/skills/interface-design, which both harnesses pull in by description
.agents/skills .claude/skills → symlink to .agents/skills
AGENTS.md (root + nested) read natively (Claude Code ≥ 2.1.277, no CLAUDE.md in repo)
beforeShellExecution pre-push hook same script, renamed pre-push.js. The Claude hook is gated with if: "Bash(git *)", so node isn't spawned for other commands. It also regenerates rules and lint-fixes the files the push includes, holding the push until the results are committed
afterFileEdit eslint --fix (per edit, Cursor only) replaced in both harnesses by eslint-fix.js on stop / Stop: one eslint --fix --cache over the uncommitted JS/TS at the end of each turn, so agent and human edits are batched (~4s per edit → ~3s per turn, ~65ms when no JS/TS changed). Errors eslint can't fix go back to the agent once per turn (Claude Stop decision: block, Cursor followup_message), the "deterministic gate" pattern from Claude Code's best-practices docs
.cursor/worktrees.json a SessionStart hook reads it directly and runs setup-worktree in a fresh claude --worktree checkout until it succeeds (one stat otherwise)
MCP none configured in either

A new agent-rules GitHub workflow runs node scripts/agent-rules.mjs --check (no install).

Verified in a real Claude Code session:

  • skills are listed, including interface-design
  • blog and packages rules attach on Read
  • the Stop hook formats a file Claude wrote, and Claude fixes an unfixable error the hook reports
  • the pre-push hook blocks a push with stale rules or unfixed lint

Open questions

Cursor documents that it also loads .claude/skills. Check once that Cursor's skill list doesn't show duplicates.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Bz2B6nekuZ6XvJbMDRfpCU


Note

Low Risk
Changes are limited to agent tooling, generated config, and CI; no runtime library or user-facing package behavior is affected.

Overview
Aligns Claude Code with Cursor’s agent setup so both harnesses share one source of truth for rules, skills, and hooks.

Rules & skills: Adds scripts/agent-rules.mjs and yarn build:agent-rules to emit .claude/rules/*.md from .cursor/rules/*.mdc (globs → paths) and symlink .claude/skills → .agents/skills. New agent-rules GitHub workflow runs --check plus hook tests. The description-only interface-design Cursor rule moves into .agents/skills/interface-design as a named skill both tools can load.

Hooks: Replaces per-edit build-skills.js / afterFileEdit eslint with pre-push.js (skill refs, agent-rules regen, eslint on files in the push) and end-of-turn eslint-fix.js (batched --fix, optional block when errors remain). Claude gets SessionStart worktree-setup.js (runs .cursor/worktrees.json in fresh worktrees). AGENTS.md, .gitignore, and ci-config docs describe the workflow.

Reviewed by Cursor Bugbot for commit e6bcdba. Bugbot is set up for automated code reviews on this repo. Configure here.

…Cursor

Cursor rules stay the single source: `yarn build:agent-rules` generates
`.claude/rules` from every `**/.cursor/rules/*.mdc` (globs become paths),
`.claude/skills` links to `.agents/skills`, and the pre-push hook plus a
new `agent-rules` CI check catch drift. AGENTS.md already loads natively.

- interface-design (description-only rule) becomes a skill, the shared
  mechanism both harnesses pull in by description
- SessionStart hook runs `.cursor/worktrees.json` setup in fresh
  `claude --worktree` checkouts
- build-skills hook renamed pre-push and also regenerates rules

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bz2B6nekuZ6XvJbMDRfpCU
@changeset-bot

changeset-bot Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: e6bcdba

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@vercel

vercel Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
docs-site Ignored Ignored Preview Oct 5, 2026 11:06pm UTC

Request Review

ntucker commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

Staff engineer (Cursor agent): LGTM at 3381781, with one follow-up to check. Nothing here needs to change in this PR.

Architecture. Keeping .cursor/rules as the only source and generating .claude/rules, with a pre-push hook and a CI drift check, copies the pattern skillReferences.mjs already uses. So this adds a familiar mechanism rather than a new kind of one. I tried the simpler-looking option of symlinking .claude/rules/*.md to the .mdc files and putting both globs and paths in the frontmatter. It isn't simpler: two glob lists would have to be kept in sync by hand (and nested rules write them from different roots), so you'd still need a check, plus symlink handling on Windows. The generator is the right choice. Requiring every rule to have globs or alwaysApply, and moving interface-design to a skill, matches where Cursor is going anyway (/migrate-to-skills does the same thing).

Checked, fine: "if": "Bash(git *)" still catches compound commands like yarn lint && git push. Claude Code's hook docs say each subcommand is matched separately, and when it can't parse a command it runs the hook anyway. Factoring regenerate() out of pre-push.js keeps the two generators uniform without adding abstraction for its own sake.

FOLLOW_UP (after merge, doesn't block): Cursor's skills docs say it loads .claude/skills/ as well as .agents/skills/ "for compatibility". With .claude/skills pointing at ../.agents/skills, Cursor might list every skill twice. Claude Code documents that it de-dupes symlinked skill folders; Cursor's docs don't say either way. The PR verified the Claude Code side only, so please open Customize → Skills in a Cursor session once and confirm each skill shows up a single time. If they're doubled, there isn't a clean in-repo fix, since Codex and Cursor need .agents/skills and Claude Code only reads .claude/skills. In that case it's a Cursor-side issue to report, or a cost we accept knowingly, and worth a one-line note in AGENTS.md either way.

@codecov

codecov Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.10%. Comparing base (7954e6b) to head (e6bcdba).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #4207   +/-   ##
=======================================
  Coverage   98.10%   98.10%           
=======================================
  Files         166      166           
  Lines        3167     3167           
  Branches      626      626           
=======================================
  Hits         3107     3107           
  Misses         18       18           
  Partials       42       42           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cursor cursor 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.

Stale Bugbot comment from a previous run.

Comment thread .claude/hooks/worktree-setup.js Outdated
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bz2B6nekuZ6XvJbMDRfpCU
@ntucker

ntucker commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

Staff engineer (Cursor agent): CHANGE_THIS_PR at 8db4197, one small fix. Moving eslint --fix from per edit to once per push, shared by both harnesses, is the right call and keeps hooks cheap like AGENTS.md now says. The lint config isn't type-aware, so running it over the branch's files each push costs little.

CHANGE_THIS_PR: lintFix() should skip uncommitted files unless the same command commits. regenerate() already has this rule ("the generator reads the working tree, so it can only vouch for what's pushed when that includes these edits; otherwise leave it to CI"). lintFix() doesn't follow it. It lints committed files straight from the working tree, so a file the branch already changes but that also has uncommitted edits gets fixed on disk, uncommitted part included. Example: foo.ts is committed and lint-clean, the agent has unformatted work in progress in it, and runs plain git push. The hook rewrites foo.ts, sees it changed, and denies the push with "Commit them, then push again." The commits being pushed were fine, and following that instruction makes the agent commit and push its unfinished edits. Suggested fix, same rule as regenerate():

const files = (commits ? [...new Set([...committed, ...dirty])] : committed.filter(file => !dirty.includes(file))).filter(...)

A dirty file then goes unchecked on that push, and CI's lint catches it, same as the generators. I tried to argue against this: pushing with uncommitted edits to a branch file isn't the usual flow, but it does happen (pushing a checkpoint mid-task), and the hook's failure mode is to push work in progress. The fix is one line and makes the two checks in this file behave the same, so it's worth doing here.

Still open from Bugbot (needs a fix or an explicit waive before merge): "Failed worktree setup is not retried" on .claude/hooks/worktree-setup.js. It's real. A failed yarn install or yarn build leaves node_modules, so later sessions skip setup, and only the first session gets the failure message. A simple fix is to gate on a marker that's written only after the last command succeeds (for example node_modules/.worktree-setup-done) instead of on node_modules existing.

Nit: the PR body still lists per-edit eslint --fix under Open questions. This commit settles it, so update that section.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bz2B6nekuZ6XvJbMDRfpCU

ntucker commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

Staff engineer (Cursor agent): At b5e5ad2, the Bugbot item "Failed worktree setup is not retried" is fixed. worktree-setup.js now skips only when node_modules/.worktree-setup-done exists, and that marker is written only after every command succeeds, so a failed install or build reruns next session. Existing worktrees without the marker will rerun setup once, which is harmless.

Still open: the lintFix() CHANGE_THIS_PR from my previous comment (skip files with uncommitted edits unless the same command commits, matching regenerate()), plus the PR-body nit about the per-edit eslint --fix open question. This commit landed seconds after that comment, so it doesn't address those yet.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bz2B6nekuZ6XvJbMDRfpCU

ntucker commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

Staff review at 8db4197, all three addressed:

  • lintFix() uncommitted edits: confirmed (a committed file with WIP edits got rewritten and the push denied). Fixed in 38c4a6a with the same rule as regenerate(). Verified a plain git push with a dirty committed file no longer denies, and git commit -am x && git push still lint-fixes it.
  • Bugbot worktree retry: fixed in b5e5ad2 (node_modules/.worktree-setup-done marker written after the last command succeeds).
  • PR body Open questions: already updated after the ESLint commit; only the Cursor skills-duplication check remains.

Generated by Claude Code

ntucker commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

Staff engineer (Cursor agent): LGTM at 38c4a6a. The lintFix() CHANGE_THIS_PR is resolved: it now uses the same rule as regenerate(), linting committed plus dirty files only when the command commits, and skipping committed files with uncommitted edits otherwise, so a plain git push no longer rewrites WIP or tells the agent to commit it. Together with the worktree-setup marker fix in b5e5ad2, nothing in the code needs to change in this PR.

One small nit that's still open: the PR body's Open questions section still says per-edit eslint --fix has no Claude twin and is waiting on a decision (checked at 20:16:44 UTC). The edit may not have saved. It should say that lint moved to the pre-push hook for both harnesses, leaving only the Cursor .claude/skills duplication check. The FOLLOW_UP from my first comment still stands for after merge.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bz2B6nekuZ6XvJbMDRfpCU

ntucker commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

Staff engineer (Cursor agent): LGTM still holds at dca38d4e. The new commit is a pure cleanup of lintFix() in pre-push.js: same dirty-file rule (skip files with uncommitted edits unless the command commits), a tighter [cm]? extension regex, an explicit [] return instead of [0], and eslint --cache. I checked that .eslintcache is already in .gitignore, so the cache file won't show up as dirty and trip the hook on the next push.

The PR-body nit is resolved too: Open questions now only lists the Cursor duplicate-skills check. That stays an after-merge FOLLOW_UP and doesn't block this PR. Bugbot is still running on this SHA.

claude added 2 commits October 5, 2026 20:47
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bz2B6nekuZ6XvJbMDRfpCU
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bz2B6nekuZ6XvJbMDRfpCU

ntucker commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

Staff engineer (Cursor agent): LGTM still holds at aced1f1. Nothing here needs a code change.

What's new (c2682f4, aced1f1): eslint-fix.js comes back as an end-of-turn hook (Cursor stop, Claude Code Stop) that fixes every uncommitted JS/TS file once per turn. pre-push.js now imports the same eslintFix() instead of keeping its own copy.

Architecture. Pulling eslintFix() into one module that both hooks share is the right shape. The two hooks also cover different files without overlapping much. The end-of-turn run catches what the agent leaves uncommitted, and the pre-push run still catches files committed earlier in the turn, which the end-of-turn run never sees because they aren't dirty anymore. So keeping both is justified, not duplication. Batching per turn instead of per edit also keeps the cost from the original per-edit hook from coming back.

Checked, fine:

  • It runs on every dirty file, including ones a person is editing in the same checkout, not only the agent's edits. The header comment says that's intended, and the result is the same as format-on-save, so I don't think it needs to change. If someone does see their editor report "file changed on disk" after an agent turn, this is the cause.
  • It always exits 0 and prints nothing, so it can't block a stop or start a loop in either harness. Deleted files and odd quoted paths from git status --porcelain are dropped by the existsSync filter instead of crashing it. --cache keeps repeat turns cheap.

Nit (PR body only): the Solution table's afterFileEdit row still says eslint --fix moved to once per push. Now it runs once per turn (eslint-fix.js on stop) plus once per push, so that row should be updated to match the new AGENTS.md line.

The after-merge FOLLOW_UP from my first comment (check once that Cursor doesn't list each skill twice through the .claude/skills symlink) is unchanged. Bugbot was still running on this SHA when I posted this.

…share hook helpers

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bz2B6nekuZ6XvJbMDRfpCU

ntucker commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

Staff engineer (Cursor agent): LGTM at eed7844. Skipping eslint when no dirty JS/TS changed since the last run is a good cut (the ~3s per turn only goes away when nothing changed), and sharing projectDir / dirtyFiles() / eslintFix() from eslint-fix.js removes the duplicated git status parsing in pre-push.js. The PR-body nit is fixed too; the Solution row now describes the stop / Stop hook.

FOLLOW_UP (doesn't block this PR, fine to fold in now if you're already in there): the end-of-turn skip uses .eslintcache's mtime as "when this hook last linted", but that file is shared with every other eslint --cache run.

  • pre-push.js writes the same cache while linting only the pushed files. So if a push that doesn't commit happens mid-turn while file A is still dirty, A's mtime ends up older than the cache and the Stop hook skips A even though it was never linted. Today the pre-push lint catches A when it's eventually pushed, so the cost is one extra push retry, not bad code landing.
  • If someone later adds --cache to yarn lint (a common tweak), every full lint would quietly advance the marker the same way.
  • Small race: a human edit landing while eslint is running gets an mtime older than the cache write, so it's skipped next turn.

A simpler fix is a marker that only the end-of-turn hook writes, stamped with the time the run started (e.g. node_modules/.cache/eslint-fix-last-run, the same pattern as .worktree-setup-done). Do const start = Date.now(), lint, then fs.utimesSync(marker, start / 1000, start / 1000). That keeps the skip and drops the coupling to eslint's cache internals, including the assumption that eslint rewrites the cache on every run.

Merge stays Nathaniel's call.

…ntcache

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bz2B6nekuZ6XvJbMDRfpCU

ntucker commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

Folded the .eslintcache follow-up into 99b457f. Confirmed that pre-push.js writes the same cache, so the coupling was real.

The end-of-turn hook now keeps its own marker, node_modules/.cache/eslint-fix-last-run, which only that hook writes. The marker's mtime is set to the time the run started, so an edit that lands while eslint is running gets linted on the next turn. Stamping the start time alone would have made every file eslint fixed look newer than the marker, so they'd be re-linted next turn. To avoid that, the marker also stores the mtimes eslint left behind on the files it linted, and the next run skips a file whose mtime still matches.

Checked locally: a dirty file gets fixed (about 2.4s), the next turn with nothing new takes 67ms, and a later edit to the same file gets linted again.


Generated by Claude Code

ntucker commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

Staff engineer (Cursor agent): LGTM at 99b457f.

This resolves my earlier FOLLOW_UP: the end-of-turn hook now keeps its own marker in node_modules/.cache/eslint-fix-last-run instead of reading .eslintcache's mtime, so a pre-push or manual eslint --cache run can no longer make the hook skip files it never fixed.

I checked the edge cases and they hold up:

  • Stamping the marker with the run's start time means edits made while eslint is running get picked up next turn.
  • The stored per-file mtimes keep eslint's own --fix rewrites (which land after the start stamp) from triggering a pointless re-run next turn, while a real later edit changes the mtime and gets linted.
  • Deleted files have no mtime and drop out, and a missing eslint install still fails open to CI with the marker advancing, which is fine.

The only gap is a sub-second race where someone edits a file after eslint has fixed it but before the marker is written; that edit would wait until it's touched again or until pre-push. That's not worth more code.

No changes requested.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bz2B6nekuZ6XvJbMDRfpCU

ntucker commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

Staff engineer (Cursor agent): LGTM at e8d9cce. No changes requested.

What's new (e8d9cce): eslint-fix.js now runs eslint with --format json, collects the severity-2 messages it couldn't fix, and hands them back to the agent once per turn: {decision: 'block', reason} for Claude Code Stop, {followup_message} for Cursor stop. pre-push.js just destructures { fixed } and still leaves unfixable errors to CI.

Checked, fine:

  • The loop guard holds in both harnesses. Claude's second Stop has stop_hook_active, and Cursor's follow-up has loop_count > 0, so it reports at most once per turn and can't spin. Skipping a Cursor aborted or error stop is right too, since nobody is there to act on it.
  • It fails open the same way as before. Only exit 1 (lint errors left) is parsed; a crash or config error (exit 2) or a missing install yields no report, and a non-JSON report is swallowed. eslint's stdout is piped and stderr ignored, so the hook's single console.log is the only stdout, which both harnesses need.
  • It only reports on files changed since the last run (the marker from 99b457f), so an old error in an untouched file isn't re-nagged every turn. The follow-up stop lints what the agent edited and records it without reporting again. Anything still broken after that is caught by CI, which is the right cut.
  • Fatal parse errors come through too (severity 2, ruleId null), and the 50-line cap keeps a broken codemod from flooding the context.

Optional, not blocking:

  • Since the hook covers files someone is editing alongside the agent, the agent can be told to fix a half-typed file that isn't its own. A one-line wording tweak (e.g. "Fix the ones in files you edited this turn") would let it skip those. Fine to leave it as is if that never comes up in practice.
  • fs.readFileSync(0) waits on a TTY when the script is run by hand from a terminal, so the // run by hand comment only holds with < /dev/null. A process.stdin.isTTY ? '{}' : … guard would fix it, but it doesn't matter for either harness.

The after-merge FOLLOW_UP from my first comment (check once that Cursor doesn't list each skill twice through the .claude/skills symlink) still stands. Merge stays Nathaniel's call.

…n a TTY

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bz2B6nekuZ6XvJbMDRfpCU

ntucker commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

Folded both optional notes into 0b4edca:

  • Scoped wording. The follow-up now asks the agent to fix the errors in files it edited and leave the rest to whoever is editing them, so a file someone else is halfway through typing is left alone.
  • No hang from a terminal. Stdin is read only when it isn't a TTY. A run from a terminal now finishes right away; verified under a pseudo-TTY.

The duplicate-skills check in Cursor is still open for Nathaniel after merge.


Generated by Claude Code

Co-authored-by: Nathaniel Tucker <me@ntucker.me>

@cursor cursor 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.

Stale Bugbot comment from a previous run.

Comment thread .cursor/hooks/eslint-fix.js
cursoragent and others added 2 commits October 5, 2026 22:01
…y linted

The stop hook stamped LAST_RUN with the run's start time so edits during
eslint are picked up next turn, but then stored each chosen file's mtime
from after the run. A save that landed on one of those files matched
linted[file], so the next turn skipped it until the file was touched again.
Remember the mtime from when the file was chosen, and only replace it with
the mtime eslint left when that fix is still the bytes on disk.

Co-authored-by: Nathaniel Tucker <me@ntucker.me>
Co-authored-by: Nathaniel Tucker <me@ntucker.me>

ntucker commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

Staff engineer (Cursor agent): Reviewed 43bac4a + 7034567 (tip 70345678).

The fix itself looks right: storing the mtime from when each file was chosen, and only swapping in eslint's post-run mtime when the file still holds eslint's output, closes the hole where an edit landing mid-run got recorded as already linted. The /-normalized relative() and the Array.isArray guard are good hardening too.

CHANGE_THIS_PR (small): the new .cursor/hooks/eslint-fix.test.js never runs in CI. Jest's roots are packages/*/src (+ Playground), and agent-rules.yml only runs scripts/agent-rules.mjs --check. This hook has needed several rounds of mtime bookkeeping fixes on this PR alone, so a test that only runs by hand will drift the first time someone edits eslint-fix.js. The test only needs node + git (it fakes eslint), so it fits the existing no-install job:

  • add a step node --test .cursor/hooks/ to .github/workflows/agent-rules.yml
  • add '.cursor/hooks/**' to both paths lists there
  • update the agent-rules.yml line in .cursor/rules/ci-config.mdc (and regenerate .claude/rules/ci-config.md) to mention it also runs the hook tests

I checked whether this is over-reach: the alternative is dropping the test, but it is the only thing guarding the race this commit fixes, and wiring it in is ~4 lines with no install cost. Nothing else blocking.

ntucker commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

Grok Bot — upcoming cloud agent push on claude/project-thread-70f0pv:

Addressing Staff CHANGE_THIS_PR on #4207 (comment): wire .cursor/hooks node tests into agent-rules.yml, update paths + ci-config docs, regenerate Claude rules.

Co-authored-by: Nathaniel Tucker <me@ntucker.me>
@cursor

cursor Bot commented Oct 5, 2026

Copy link
Copy Markdown

Done in c39e937, addressing the CHANGE_THIS_PR in #4207 (comment):

  • agent-rules.yml: new Test agent hooks step (node --test .cursor/hooks/), and '.cursor/hooks/**' added to both paths lists
  • ci-config.mdc agent-rules.yml line now mentions the hook tests; .claude/rules/ci-config.md regenerated (agent-rules.mjs --check passes)

Verified on Node 26.10 (the workflow's version): node --test .cursor/hooks/ finds eslint-fix.test.js and runs all 4 tests (pass). Note it relies on Node ≥ 21 directory discovery: on Node 22.14 a bare directory argument runs as one no-op "test", so keep the workflow's node-version at 21+.

@cursor

cursor Bot commented Oct 5, 2026

Copy link
Copy Markdown

Correction to the note above: directory discovery for node --test <dir> is not in Node 21/22 (22.14 runs the bare directory as one no-op test). It works on 26.10, the workflow's version, so the requirement is only to keep node-version: '26'. I didn't pin down the exact version where it landed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bz2B6nekuZ6XvJbMDRfpCU
@ntucker

ntucker commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

Staff engineer (Cursor agent): Reviewed c39e937 (tip c39e937f). This resolves the CHANGE_THIS_PR in #4207 (comment).

agent-rules.yml now runs node --test .cursor/hooks/ in the existing no-install job, .cursor/hooks/** is in both paths lists, and the ci-config rule plus its Claude copy say so. The agent-rules run on this SHA ran all 4 hook tests and they passed. Note that node --test on the directory only picks up *.test.js there, so eslint-fix.js itself isn't executed as a test.

Staff LGTM, no open asks. Merge only on Nathaniel's say-so.

ntucker commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

Following up on c39e937: the step now runs node --test '.cursor/hooks/*.test.js' instead of passing a bare directory.

I checked locally on Node 22.22. node --test .cursor/hooks/ reported a single passing "test" named after the directory and never loaded the test file. The glob ran all 4 tests. Since a glob works on every Node version from 21 up, the hook tests no longer depend on which Node the workflow pins.

ci-config.mdc and the regenerated .claude/rules/ci-config.md now match the new step, and agent-rules.mjs --check passes.


Generated by Claude Code

@ntucker ntucker left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

One actionable issue in the pre-push hook's handling of partial commits.

Comment thread .cursor/hooks/pre-push.js Outdated
claude added 4 commits October 5, 2026 22:25
…mmit fully takes

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bz2B6nekuZ6XvJbMDRfpCU
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bz2B6nekuZ6XvJbMDRfpCU
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bz2B6nekuZ6XvJbMDRfpCU

ntucker commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator Author

The agent-rules failure on 87c24ab was this PR's own: #4205 changed .cursor/rules/ci-config.mdc on master, and CI checks the merge result, so the generated .claude/rules/ci-config.md was stale against it. I merged master and regenerated the rule in 01b9972. agent-rules.mjs --check and the hook tests pass locally.

That same rule from #4205 also says master runs must never be cancelled. agent-rules.yml's concurrency group used github.ref, which is the same for every master push, so it could cancel master runs. It now uses github.run_id, the same as skills.yml (d1cfe5e).


Generated by Claude Code

@cursor cursor 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.

Stale Bugbot comment from a previous run.

Comment thread .cursor/hooks/pre-push.js
Comment thread .cursor/hooks/pre-push.js
Comment thread .cursor/hooks/eslint-fix.js
…ake message text for -a; retry after an eslint crash

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bz2B6nekuZ6XvJbMDRfpCU

@cursor cursor 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.

Stale Bugbot comment from a previous run.

Comment thread .cursor/hooks/pre-push.js Outdated
… fix

Pending fixes are kept with the file's HEAD blob, so a commit that changes the
file clears them, and a push that runs `git add` in the same command isn't held.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bz2B6nekuZ6XvJbMDRfpCU

@cursor cursor 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.

Stale Bugbot comment from a previous run.

Comment thread .cursor/hooks/pre-push.js Outdated
…s them

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bz2B6nekuZ6XvJbMDRfpCU

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f8e9542. Configure here.

Comment thread .cursor/hooks/pre-push.js Outdated
Comment thread .cursor/hooks/pre-push.js Outdated
Guessing what a `git add` in the same command stages kept missing cases
(pathspecs with -A, cd, Windows paths). Holding costs one extra push.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bz2B6nekuZ6XvJbMDRfpCU
@ntucker
ntucker merged commit 1106011 into master Oct 5, 2026
26 checks passed
@ntucker
ntucker deleted the claude/project-thread-70f0pv branch October 5, 2026 23:09
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.

3 participants