Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .cursor/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"version": 1,
"hooks": {
"afterFileEdit": [
{
"command": ".cursor/hooks/after-file-edit.sh",
"matcher": "Write|StrReplace"
}
],
"postToolUse": [
{
"command": ".cursor/hooks/post-shell-hint.sh",
"matcher": "Shell"
}
]
}
}
66 changes: 66 additions & 0 deletions .cursor/hooks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Cursor agent hooks

Project-level [Cursor hooks](https://cursor.com/docs/agent/hooks) automate checks around agent file edits and shell commands. Configuration lives in [`.cursor/hooks.json`](../hooks.json); scripts run from the repository root.

## Registered hooks

| Event | Script | Matcher | Purpose |
|-------|--------|---------|---------|
| `afterFileEdit` | `after-file-edit.sh` | `Write`, `StrReplace` | Optionally run [prek](https://prek.j178.dev/) on the edited file (lint/format aligned with `.pre-commit-config.yaml`). |
| `postToolUse` | `post-shell-hint.sh` | `Shell` | After shell commands, suggest targeted backend tests when a matching test file exists. |

Both hooks **fail open** (errors do not block the agent) unless you change the scripts or set `failClosed: true` in `hooks.json`.

## Opt-in: prek after agent edits

By default, `after-file-edit.sh` is a no-op so agent sessions stay fast. To run prek on each agent-edited file under `backend/` or `frontend/`:

```bash
export CURSOR_RUN_PREK_ON_EDIT=1
```

Requires `uv` on `PATH` (same as [development.md](../../development.md#pre-commits-and-code-linting)). Manual equivalent:

```bash
uv run prek run --files path/to/changed/file
```

## Post-shell test hints

After a `Shell` tool call that touches `backend/app/**/*.py`, `post-shell-hint.sh` looks for a corresponding `backend/tests/**/test_*.py` and returns `additional_context` suggesting:

```bash
cd backend && uv run pytest path/to/test_file.py
```

This does not run pytest automatically (tests may need Docker/DB). Run tests when you are ready.

## Adding or changing hooks

1. Edit `.cursor/hooks.json` (schema `version: 1`).
2. Add scripts under `.cursor/hooks/` with a shebang; make them executable (`chmod +x`).
3. Use the narrowest [hook event](https://cursor.com/docs/agent/hooks) and a `matcher` when possible.
4. Reload: save `hooks.json` or restart Cursor; verify in **Settings → Hooks** or the **Hooks** output channel.

Example — gate destructive git commands (`failClosed: true`):

```json
{
"version": 1,
"hooks": {
"beforeShellExecution": [
{
"command": ".cursor/hooks/block-force-push.sh",
"matcher": "git push.*--force",
"failClosed": true
}
]
}
}
```

## Safety conventions

- Prefer **audit or suggest** over auto-running heavy commands (full `prek --all-files`, full pytest suite) during agent loops.
- Do not commit secrets; hooks read stdin JSON only — no credential files.
- Match [CONTRIBUTING.md](../../CONTRIBUTING.md) and pre-commit rules; hooks complement, not replace, `uv run prek install` before `git commit`.
38 changes: 38 additions & 0 deletions .cursor/hooks/after-file-edit.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# Optional prek on agent-edited files. See .cursor/hooks/README.md
set -euo pipefail

if [[ "${CURSOR_RUN_PREK_ON_EDIT:-}" != "1" ]]; then
exit 0
fi

input=$(cat)
file_path=$(printf '%s' "$input" | python3 -c '
import json, sys
try:
data = json.load(sys.stdin)
except json.JSONDecodeError:
sys.exit(0)
path = data.get("file_path") or data.get("path") or ""
if path:
print(path)
' 2>/dev/null || true)

[[ -n "${file_path:-}" ]] || exit 0

repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$repo_root"

case "$file_path" in
backend/*|frontend/*)
;;
*)
exit 0
;;
esac

if command -v uv >/dev/null 2>&1; then
uv run prek run --files "$file_path" || true
fi

exit 0
45 changes: 45 additions & 0 deletions .cursor/hooks/post-shell-hint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# Suggest targeted pytest after backend shell edits. See .cursor/hooks/README.md
set -euo pipefail

repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
input=$(cat)
hint=$(printf '%s' "$input" | REPO_ROOT="$repo_root" python3 -c '
import json, os, re, sys
from pathlib import Path

try:
data = json.load(sys.stdin)
except json.JSONDecodeError:
sys.exit(0)

command = data.get("command") or data.get("tool_input", {}).get("command") or ""
if not command:
sys.exit(0)

repo = Path(os.environ["REPO_ROOT"])
matches = re.findall(r"backend/app/[\w./-]+\.py", command)
if not matches:
sys.exit(0)

module_path = matches[-1]
stem = Path(module_path).stem
tests_dir = repo / "backend" / "tests"
if not tests_dir.is_dir():
sys.exit(0)

candidates = sorted(tests_dir.rglob(f"test_{stem}.py"))
if not candidates:
sys.exit(0)

rel = candidates[0].relative_to(repo / "backend")
print(f"If you changed {module_path}, consider: cd backend && uv run pytest {rel}")
' 2>/dev/null || true)

if [[ -n "${hint:-}" ]]; then
python3 -c 'import json,sys; print(json.dumps({"additional_context": sys.argv[1]}))' "$hint"
else
echo '{}'
fi

exit 0