Skip to content

fix(hooks): read the workspace from Cursor plugin hooks - #3

Merged
sebastian-iancu merged 2 commits into
mainfrom
fix/cursor-hooks
Oct 27, 2026
Merged

sebastian-iancu merged 2 commits into
mainfrom
fix/cursor-hooks

Conversation

@sebastian-iancu

@sebastian-iancu sebastian-iancu commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Cursor runs a plugin hook from the plugin's install directory and reads JSON on stdout, not plain text (Cursor hooks). Both PHP hooks read the workspace from that install directory and printed plain text there.

  • session-start.sh detects Cursor by CURSOR_PLUGIN_ROOT, cds to CURSOR_PROJECT_DIR and returns its line as JSON additional_context, escaped. On Cursor the line names /php-coding; Claude Code still gets the plain Load the php-coding skill line.
  • format-on-save.sh stays on afterFileEdit, for which Cursor documents no output; it only rewrites the file. It cds to CURSOR_PROJECT_DIR, so the fixer config, vendor/bin/php-cs-fixer and a relative file_path all resolve in the workspace.
  • Without CURSOR_PROJECT_DIR both hooks stay silent. Falling back to the install would read it as a PHP workspace, because it ships references/*.php.
  • hooks/cursor-hooks.json has "version": 1 and a timeout (in seconds) for each hook.
  • scripts/validate.py fails a Cursor hooks file without an integer "version": 1, with a non-object top level, or with ${CLAUDE_PLUGIN_ROOT}.
  • AGENTS.md cites Cursor's hooks page with a check date, and docs/testing.md replays both hooks with Cursor's variables set.

There is no skill-nudge hook in this plugin, so nothing needed to move to postToolUse.

Not in this PR: in a multi-root Cursor workspace, the formatter uses the config of the root CURSOR_PROJECT_DIR names. Finding the config by searching upward from the edited file would fix that, but it would also change which config Claude Code picks in a monorepo, so it belongs in its own change.

Test plan

  • python3 scripts/validate.py and claude plugin validate .
  • Session start: Claude Code prints the plain line. Cursor prints JSON that python3 -m json.tool accepts, naming /php-coding, guzzle.md and Laravel Boost. It is silent when CURSOR_PROJECT_DIR is unset, empty or missing, and in a non-PHP project.
  • Format on save, with a stub fixer: a Claude Code relative path and a Cursor absolute or relative path each run the fixer in the project with the project's config. Without CURSOR_PROJECT_DIR the fixer does not run, and a path key no longer wins over file_path.
  • Validator on mutated copies: "version": true, 1.0 or missing, a top-level list, and ${CLAUDE_PLUGIN_ROOT} each fail. A missing file is reported once.
  • Install this branch as a Cursor plugin in a PHP repo and confirm the session line arrives and a .php edit is reformatted.

Made with Cursor

🤖 Generated with Claude Code

A Cursor plugin hook runs in the plugin install and only accepts
additional_context from sessionStart. session-start now inspects
CURSOR_PROJECT_DIR, and format-on-save finds the fixer config there.
The formatter stays on afterFileEdit.

Co-authored-by: Cursor <cursoragent@cursor.com>
@sebastian-iancu
sebastian-iancu merged commit ad262c4 into main Oct 27, 2026
1 check passed
Without CURSOR_PROJECT_DIR both hooks now stay silent instead of reading
the plugin install, which ships references/*.php and read as a PHP
workspace. The session line is JSON-escaped for Cursor. format-on-save
drops the unneeded "path" key and the relative-path join the cd already
covers.

The validator keeps its "when present" guard, rejects a non-integer
version and a top-level non-object in cursor-hooks.json. The docs cite
Cursor's hooks page with a check date, testing.md replays both hooks
with Cursor's variables set, and the CHANGELOG is split per change.

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