Skip to content

Commit 97cc381

Browse files
backnotpropclaude
andauthored
cursor update & path extraction utility (#77)
* feat(cursor): Add project-level hooks support Cursor now supports project-level hooks at `.cursor/hooks.json` in addition to user-level hooks at `~/.cursor/hooks.json`. Changes: - Update CursorHarness::settings_path() to respect global parameter - Project init creates .cursor/hooks.json (project-level) - Global init creates ~/.cursor/hooks.json (user-level) - Update example scripts to use project-level hooks - Fix check_appointment_time.py to handle patient_name lookups - Add tests for cursor init hook locations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * cleanup repo and examples * feat(cursor): Add lifecycle events, schema updates, and stop continuation New lifecycle events (fire-and-forget): - afterShellExecution: command, output, duration - afterMCPExecution: tool_name, tool_input, result_json, duration - afterAgentResponse: text - afterAgentThought: text, duration_ms Schema updates: - Common fields: model, cursor_version, user_email (all optional) - Response fields: snake_case (user_message, agent_message) - beforeSubmitPrompt: user_message on block - stop: loop_count input field Stop hook agent looping: - Block decision returns followup_message - Cursor submits as next user message (max 5 auto-followups) - Mirrors Claude Code's block+reason pattern 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * clean up example for cursor * more robust form of protected paths * command path extraction is proper * finalize cursor example * chore: fmt and checks * introduce shell-words for command string parsing * test readme idea * test readme idea * test readme idea * test readme idea * test readme idea * test readme idea * test readme idea * test readme idea * test readme idea * test readme idea * test readme idea --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent ef3a6ea commit 97cc381

65 files changed

Lines changed: 2356 additions & 11975 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,6 @@ notes.note
103103
# todo: considering enabling again later
104104
.claude/commands
105105
temp-reference/
106+
107+
108+
temp-specs

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,18 @@ Make AI agents follow the rules.
2222
- **LLM-as-a-judge** for more dynamic governance.
2323
- **Trigger alerts** and put _bad_ agents in timeout when they repeatedly violate rules.
2424

25-
Cupcake intercepts agent events and evaluates them against **user-defined rules** written in **[Open Policy Agent (OPA)](https://www.openpolicyagent.org/) [Rego](https://www.openpolicyagent.org/docs/policy-language)** or **Typescript policy programs** that abstract Rego. Agent actions can be blocked, modified, and auto-corrected by providing the agent helpful feedback. Additional benefits include reactive automation for tasks you dont need to rely on the agent to conduct (like linting after a file edit).
25+
Cupcake intercepts agent events and evaluates them against **user-defined rules** written in **[Open Policy Agent (OPA)](https://www.openpolicyagent.org/) [Rego](https://www.openpolicyagent.org/docs/policy-language).** Agent actions can be blocked, modified, and auto-corrected by providing the agent helpful feedback. Additional benefits include reactive automation for tasks you dont need to rely on the agent to conduct (like linting after a file edit).
2626

2727
## Updates
2828

2929
**`2025-12-09`**: Official open source release. Roadmap will be produced in Q1 2026.
3030

3131
**`2025-04-04`**: We produce the [feature request](https://github.com/anthropics/claude-code/issues/712) for Claude Code Hooks. Runtime alignment requires integration into the agent harnesses, and we pivot away from filesystem and os-level monitoring of agent behavior (early cupcake PoC).
3232

33+
[Follow on X for a regular updates.](https://x.com/CupcakeSecures)
34+
35+
## Resources
36+
3337
## Supported Agent Harnesses
3438

3539
Cupcake provides lightweight **native integrations** for multiple AI coding agents:

cupcake-cli/src/CLAUDE.md

Lines changed: 37 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
# Cursor Harness Configuration
22

3-
## Critical Difference from Claude Code
3+
## Project-Level and User-Level Hooks
44

5-
**Cursor hooks MUST be installed globally** at `~/.cursor/hooks.json`. Unlike Claude Code which supports both project-level (`.claude/settings.json`) and global (`~/.claude/settings.json`) configurations, **Cursor does not support project-level hooks**.
5+
Cursor now supports both project-level and user-level hooks, similar to Claude Code.
66

7-
Reference: [Cursor Hooks Documentation](https://cursor.com/docs/agent/hooks.md)
7+
**Hook Priority Order (highest to lowest):** Enterprise → Project → User
88

9-
## The Configuration Behavior
9+
Reference: [Cursor Hooks Documentation](https://docs.cursor.com/context/hooks)
10+
11+
## Configuration Behavior
1012

1113
### `cupcake init --harness cursor` (Project Init)
1214

13-
Creates hooks at `~/.cursor/hooks.json` (global) with **relative policy paths**:
15+
Creates hooks at `.cursor/hooks.json` (project-level) with **relative policy paths**:
1416

1517
```json
1618
{
@@ -36,7 +38,7 @@ Creates hooks at `~/.cursor/hooks.json` (global) with **relative policy paths**:
3638

3739
### `cupcake init --global --harness cursor` (Global Init)
3840

39-
Creates hooks at `~/.cursor/hooks.json` (global) with **absolute policy paths**:
41+
Creates hooks at `~/.cursor/hooks.json` (user-level) with **absolute policy paths**:
4042

4143
```json
4244
{
@@ -59,20 +61,23 @@ Creates hooks at `~/.cursor/hooks.json` (global) with **absolute policy paths**:
5961
#### `CursorHarness::settings_path()`
6062

6163
```rust
62-
fn settings_path(&self, _global: bool) -> PathBuf {
63-
// Cursor hooks MUST always be in ~/.cursor/hooks.json (global)
64-
// Cursor does not support project-level hooks like Claude Code does.
65-
// The hooks are always read from the user's home directory.
66-
// Reference: https://cursor.com/docs/agent/hooks.md
67-
dirs::home_dir()
68-
.unwrap_or_else(|| PathBuf::from("~"))
69-
.join(".cursor")
70-
.join("hooks.json")
64+
fn settings_path(&self, global: bool) -> PathBuf {
65+
// Cursor now supports both project-level and user-level hooks
66+
// Priority order: Enterprise → Project → User
67+
// Reference: https://docs.cursor.com/context/hooks
68+
if global {
69+
// User-level hooks: ~/.cursor/hooks.json
70+
dirs::home_dir()
71+
.unwrap_or_else(|| PathBuf::from("~"))
72+
.join(".cursor")
73+
.join("hooks.json")
74+
} else {
75+
// Project-level hooks: .cursor/hooks.json
76+
Path::new(".cursor").join("hooks.json")
77+
}
7178
}
7279
```
7380

74-
**Key point**: The `global` parameter is ignored (prefixed with `_`). Cursor hooks are **always** written to the global location.
75-
7681
#### `CursorHarness::generate_hooks()`
7782

7883
```rust
@@ -99,18 +104,6 @@ fn generate_hooks(&self, policy_dir: &Path, global: bool) -> Result<Value> {
99104
}
100105
```
101106

102-
**Key point**: The `global` parameter determines whether to use absolute or relative paths in the hook commands, NOT the location of the hooks file.
103-
104-
## Bug History
105-
106-
### Original Bug (Fixed 2025-09-03)
107-
108-
**Problem**: `CursorHarness::settings_path()` returned `.cursor/hooks.json` (project-level) when `global=false`, but Cursor ignores project-level hook files.
109-
110-
**Result**: Running `cupcake init --harness cursor` created `.cursor/hooks.json` in the project directory, which Cursor never read.
111-
112-
**Fix**: Changed `settings_path()` to always return `~/.cursor/hooks.json` regardless of the `global` parameter.
113-
114107
## Testing
115108

116109
### Verify Project Init
@@ -119,8 +112,8 @@ fn generate_hooks(&self, policy_dir: &Path, global: bool) -> Result<Value> {
119112
cd /tmp/test-project
120113
cupcake init --harness cursor
121114

122-
# Verify hooks created at global location
123-
cat ~/.cursor/hooks.json
115+
# Verify hooks created at project location
116+
cat .cursor/hooks.json
124117

125118
# Should show relative path: --policy-dir .cupcake
126119
```
@@ -130,27 +123,26 @@ cat ~/.cursor/hooks.json
130123
```bash
131124
cupcake init --global --harness cursor
132125

133-
# Verify hooks created at global location
126+
# Verify hooks created at user location
134127
cat ~/.cursor/hooks.json
135128

136129
# Should show absolute path: --policy-dir /Users/alice/.config/cupcake
137130
```
138131

139132
## Comparison with Claude Code
140133

141-
| Aspect | Claude Code | Cursor |
142-
| ------------------------- | ------------------------------ | ------------------------- |
143-
| **Project hooks** | `.claude/settings.json`| Not supported ❌ |
144-
| **Global hooks** | `~/.claude/settings.json`| `~/.cursor/hooks.json`|
145-
| **Hook location choice** | Respects `--global` flag | Always global |
146-
| **Policy path (project)** | `$CLAUDE_PROJECT_DIR/.cupcake` | `.cupcake` (relative) |
147-
| **Policy path (global)** | Absolute path | Absolute path |
148-
| **Process cwd** | Project root (via env var) | Workspace root (direct) |
134+
| Aspect | Claude Code | Cursor |
135+
| ------------------------- | ------------------------------ | ---------------------------- |
136+
| **Project hooks** | `.claude/settings.json`| `.cursor/hooks.json` |
137+
| **User hooks** | `~/.claude/settings.json`| `~/.cursor/hooks.json` |
138+
| **Hook location choice** | Respects `--global` flag | Respects `--global` flag |
139+
| **Policy path (project)** | `$CLAUDE_PROJECT_DIR/.cupcake` | `.cupcake` (relative) |
140+
| **Policy path (global)** | Absolute path | Absolute path |
141+
| **Process cwd** | Project root (via env var) | Workspace root (direct) |
149142

150143
## Key Takeaways
151144

152-
1. **Cursor hooks are ALWAYS global** - stored at `~/.cursor/hooks.json`
153-
2. **Project init uses relative paths** - `.cupcake` resolves via workspace cwd
154-
3. **Global init uses absolute paths** - points to `~/.config/cupcake/`
155-
4. **The `global` parameter affects policy paths, not hook file location** (for Cursor)
156-
5. **Cursor spawns hooks with cwd=workspace root** - enabling relative path resolution
145+
1. **Cursor supports both project and user-level hooks** - stored at `.cursor/hooks.json` and `~/.cursor/hooks.json`
146+
2. **Project init creates project-level hooks** - `.cursor/hooks.json` with relative policy paths
147+
3. **Global init creates user-level hooks** - `~/.cursor/hooks.json` with absolute policy paths
148+
4. **Cursor spawns hooks with cwd=workspace root** - enabling relative path resolution

cupcake-cli/src/harness_config.rs

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -116,15 +116,20 @@ impl HarnessConfig for CursorHarness {
116116
"Cursor"
117117
}
118118

119-
fn settings_path(&self, _global: bool) -> PathBuf {
120-
// Cursor hooks MUST always be in ~/.cursor/hooks.json (global)
121-
// Cursor does not support project-level hooks like Claude Code does.
122-
// The hooks are always read from the user's home directory.
123-
// Reference: https://cursor.com/docs/agent/hooks.md
124-
dirs::home_dir()
125-
.unwrap_or_else(|| PathBuf::from("~"))
126-
.join(".cursor")
127-
.join("hooks.json")
119+
fn settings_path(&self, global: bool) -> PathBuf {
120+
// Cursor now supports both project-level and user-level hooks
121+
// Priority order: Enterprise → Project → User
122+
// Reference: https://docs.cursor.com/context/hooks
123+
if global {
124+
// User-level hooks: ~/.cursor/hooks.json
125+
dirs::home_dir()
126+
.unwrap_or_else(|| PathBuf::from("~"))
127+
.join(".cursor")
128+
.join("hooks.json")
129+
} else {
130+
// Project-level hooks: .cursor/hooks.json
131+
Path::new(".cursor").join("hooks.json")
132+
}
128133
}
129134

130135
fn generate_hooks(&self, policy_dir: &Path, global: bool) -> Result<Value> {

0 commit comments

Comments
 (0)