Cursor afterFileEdit payload and hook reference
A field guide to Cursor afterFileEdit input: file_path, edits, configuration scope, environment variables, failure handling, and Tab separation.
Cursor’s afterFileEdit hook fires after the Agent edits a file. Its hook-specific input contains the absolute file_path and an edits array with old_string and new_string values. It is an Agent hook, not a general file-system watcher. Cursor exposes a separate afterTabFileEdit event for Tab completions.
I am Pramod, and I work on tailtest’s Cursor integration. This page is the contract reference: what triggers the hook, what arrives on standard input, where configuration can live, and which details are observed behavior rather than part of Cursor’s documented interface. If you want the copyable test setup, use the separate Cursor afterFileEdit test hook recipe.
Updated 15 September 2026. The payload and configuration sections below were checked against the current Cursor Hooks reference. Earlier versions of this article described a project-only configuration and a payload without edit strings. Cursor’s current documented contract includes user-level configuration and an edits array, so those sections have been corrected.
The contract at a glance
| Question | Current answer |
|---|---|
| What triggers it? | A Cursor Agent file edit |
| Does Tab use it? | No. Tab has afterTabFileEdit |
| Main hook-specific fields | file_path and edits |
| Does each edit include text? | Yes. old_string and new_string |
| How is input delivered? | JSON on standard input |
| Where can hooks be configured? | Enterprise, team, project, and user levels |
| Can it prevent the edit? | No practical rollback. The event runs after the edit |
| Does Cursor expose hook environment variables? | Yes, including CURSOR_PROJECT_DIR and CURSOR_VERSION |
That first row is the important boundary. afterFileEdit reports a completed Agent edit. Manual typing, a normal save, and language-server activity are not Agent edits. Cursor documents Tab completions under a separate pair of events, beforeTabFileRead and afterTabFileEdit.
The afterFileEdit payload
Every hook receives Cursor’s common fields, including the conversation and generation identifiers, model information, hook_event_name, cursor_version, workspace_roots, user email when available, and the transcript path when enabled. afterFileEdit adds the changed file and edit records.
A representative input looks like this:
{
"conversation_id": "conversation-123",
"generation_id": "generation-456",
"hook_event_name": "afterFileEdit",
"cursor_version": "current-version",
"workspace_roots": ["/abs/path/to/project"],
"file_path": "/abs/path/to/project/src/parser.py",
"edits": [
{
"old_string": "def parse(value):",
"new_string": "def parse(value, strict=False):"
}
]
}
The two hook-specific fields have different jobs:
file_pathtells a formatter, test selector, or audit script which file changed.editsgives the replaced and replacement text for each recorded edit.
Use file_path when your next step depends on language, directory, or test ownership. Use edits only when the changed text itself matters. A test dispatcher usually needs the path and not the full replacement text.
Treat the common fields as useful context, not as substitutes for the file path. A workspace can contain more than one root, and workspace_roots can therefore hold multiple entries. Code that assumes the first root owns every changed file will break in multi-root workspaces.
afterFileEdit and afterTabFileEdit are different events
Cursor separates Agent activity from Tab completions:
afterFileEditbelongs to Agent and includesfile_pathplusold_stringandnew_stringedit records.afterTabFileEditbelongs to inline Tab completions and adds range and line details for finer post-processing.
This separation is useful for testing. An Agent turn may produce a coordinated set of edits that should be tested together. A Tab completion is a smaller editor interaction and may deserve formatting or lightweight validation instead.
In our own April 2026 logs, none of 600 observed Tab acceptances entered the Agent afterFileEdit path. The current documentation now makes that boundary explicit by naming a separate Tab event. The observed count is our evidence; the event separation is the published contract.
Where hooks.json can live
Cursor loads hook configuration from several levels:
- Enterprise configuration managed by the organization.
- Team configuration distributed through the enterprise dashboard.
- Project configuration at
<project-root>/.cursor/hooks.json. - User configuration at
~/.cursor/hooks.json.
All matching hooks can run. Cursor documents the priority order as enterprise, team, project, then user. This means a project hook does not operate in isolation merely because it is checked into the repository.
For a shared test policy, the project file is usually the right home because it travels with the code and can run for cloud agents after their workspace becomes writable. For a personal formatter or audit note, a user hook may be more appropriate.
A project-level definition can be as small as:
{
"version": 1,
"hooks": {
"afterFileEdit": [
{
"command": "python3 .cursor/hooks/collect_edit.py",
"timeout": 5
}
]
}
}
Project hook commands run from the project root. User hooks run from the user’s Cursor configuration directory. Keep relative paths aligned with that working directory.
Environment variables are part of the current contract
Cursor also exposes hook environment variables. The documented set includes:
CURSOR_PROJECT_DIR, always present.CURSOR_VERSION, always present.CURSOR_USER_EMAIL, when the user is logged in.CURSOR_TRANSCRIPT_PATH, when transcripts are enabled.CURSOR_CODE_REMOTE, for remote workspaces.CLAUDE_PROJECT_DIR, as a compatibility alias for the project directory.
Session-scoped variables returned by sessionStart are also available to later hook executions in that session.
Prefer the JSON payload for event data such as file_path and edits. Use environment variables for process context such as the project directory or Cursor version. This keeps the handler tied to the documented source for each value.
Exit behavior and failure handling
Cursor command hooks use a shared exit model:
- Exit
0means the hook succeeded and Cursor can use any supported JSON output. - Exit
2is the blocking exit code for hooks that can make a permission decision. - Other nonzero exits fail open by default and are logged.
afterFileEdit runs after the file change, so it is the wrong place for a preventive access control. A failure can report a problem, but it cannot make the completed edit never have happened. Put preventive policy in an earlier hook such as preToolUse or another relevant before-event.
The current Cursor reference documents input for afterFileEdit but does not list a hook-specific output schema for it. Keep this handler small, deterministic, and quiet. If you need the Agent to act after tests run, the stop hook has a documented followup_message output.
What tailtest does at this boundary
Tailtest uses afterFileEdit as an accumulator, not as the full test runner:
- Parse the JSON payload.
- Read and validate
file_path. - Ignore vendored, generated, or unsupported files.
- Record the changed path for the current conversation.
- Exit quickly.
The heavier work belongs at stop, where the whole Agent turn is visible. A turn often changes a service, its model, a migration, and the code that connects them. Running the full suite after each individual write repeats work and makes the hook feel slow. Recording paths per edit and testing once per turn preserves the event boundary without multiplying latency.
Across 3,400 observed afterFileEdit events in our April 2026 logs, hook-entry latency was 38 ms at p50, 71 ms at p90, and 142 ms at p99. The slow tail was dominated by cold Python startup on the first event in a session. Those measurements describe our implementation and environments, not a Cursor service-level promise.
Observed behaviors outside the published schema
Some useful engineering details come from our own testing rather than the official field list.
A file read can race the write in some setups. We observed cases where reading the changed file immediately at hook entry saw stale content. Tailtest records the path and waits until stop before reading it. This is an implementation precaution, not a documented Cursor guarantee.
Generated files create noise. Lockfiles, build output, vendored code, and generated clients can account for many events without improving test selection. Filter them before writing state.
Concurrent conversations need separate state. Cursor supplies conversation_id. Use it in the state key when more than one Agent conversation may edit the same workspace. A single shared pending-file list is acceptable only for a one-conversation example.
Cloud agents do not have user-level hooks. Project hooks are the portable choice for local and cloud use. Cursor also notes that hooks start only after a cloud workspace becomes writable, so they do not observe early read-only exploration.
Security boundary
A hook command is executable code. Project hooks run in a trusted workspace with the user’s process permissions. Review both the configuration and every referenced script before accepting a repository as trusted. Do not install an unknown hook merely because the JSON looks harmless.
For security controls, decide whether failure should block. Cursor supports failClosed for hooks where a crash, timeout, or invalid result must deny the action. A post-edit test observer is usually better kept fail-open so a broken test helper does not make the editor unusable. A secret-read guard or destructive-command gate has a different risk profile.
Where this reference fits
Use this page when you need the event contract, payload fields, configuration levels, or execution context. Use the Cursor test hook recipe when you want the project files and a two-stage collector and runner. The Cursor solution page covers tailtest itself, and hook-based testing explained covers the larger design choice.
FAQ
What fields are in the Cursor afterFileEdit payload?
The hook-specific fields are file_path and edits. Each edit contains old_string and new_string. The payload also includes Cursor’s common hook fields, such as conversation_id, generation_id, hook_event_name, cursor_version, and workspace_roots.
Does afterFileEdit fire for Cursor Tab completions?
No. Cursor documents Tab completions under the separate afterTabFileEdit event. afterFileEdit is an Agent hook.
Can I configure afterFileEdit globally?
Yes. Cursor supports user configuration at ~/.cursor/hooks.json, as well as project, team, and enterprise sources. User hooks are not available inside cloud-agent virtual machines, so use a project hook when the repository needs the behavior everywhere.
Does afterFileEdit include the changed text?
Yes. The current documented input contains an edits array whose entries include old_string and new_string.
Which environment variables do Cursor hooks receive?
The documented variables include CURSOR_PROJECT_DIR, CURSOR_VERSION, optional user and transcript values, a remote-workspace flag, and CLAUDE_PROJECT_DIR for compatibility. Event data still belongs in the JSON payload.
Should afterFileEdit run the entire test suite?
Usually no. Record changed paths in afterFileEdit, then run the relevant test cycle once at stop. The split keeps the per-edit hook fast and avoids repeating the same suite during a multi-file Agent turn.