Skip to content

Commit b487aa9

Browse files
author
PunkDevRobot
committed
fix(cli): global hooks must use project-relative policy dir (#104)
Global init (claude/cursor/factory) generated hook commands with --policy-dir pointing at the global config root, e.g. cupcake eval --harness claude --policy-dir ~/Library/Application Support/cupcake eval treats --policy-dir as the *project* root. ProjectPaths::resolve_with_config only recognizes a path that is .cupcake or contains .cupcake/, so the global root fell into the legacy branch and resolved a bogus project dir (<root>/../.cupcake/policies/<harness>). Result: project-local policies were never read, and pre-fix the nonexistent path aborted eval with exit code 1. The global config is auto-discovered by the engine independently of --policy-dir. Global hooks should evaluate the project at the harness working directory just like project hooks, so emit the same project-relative path (/.cupcake for claude, .cupcake for cursor, $FACTORY_PROJECT_DIR/.cupcake for factory) instead of the global root. Depends on the missing-project-policies-dir fix: under the global hook a project without a local .cupcake now resolves to a (correct) nonexistent project path and proceeds with global policies only. Update the global-init assertion and the Cursor harness CLAUDE.md doc to match. Existing global hooks need 'cupcake init --global' re-run. Verified: init --global --harness claude now emits --policy-dir $CLAUDE_PROJECT_DIR/.cupcake.
1 parent e631608 commit b487aa9

3 files changed

Lines changed: 38 additions & 51 deletions

File tree

cupcake-cli/src/CLAUDE.md

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,20 +38,22 @@ Creates hooks at `.cursor/hooks.json` (project-level) with **relative policy pat
3838

3939
### `cupcake init --global --harness cursor` (Global Init)
4040

41-
Creates hooks at `~/.cursor/hooks.json` (user-level) with **absolute policy paths**:
41+
Creates hooks at `~/.cursor/hooks.json` (user-level) with the **same project-relative policy path** as project init:
4242

4343
```json
4444
{
4545
"version": 1,
4646
"hooks": {
4747
"beforeShellExecution": [{
48-
"command": "cupcake eval --harness cursor --policy-dir /Users/alice/.config/cupcake"
48+
"command": "cupcake eval --harness cursor --policy-dir .cupcake"
4949
}],
5050
...
5151
}
5252
}
5353
```
5454

55+
The global hook fires in every workspace and evaluates that workspace's `.cupcake` (or none). The org-wide global config is auto-discovered by the engine independently of `--policy-dir`, so the hook must NOT point at the global config root — doing so mis-resolved project paths and skipped local policies (issue #104 follow-up).
56+
5557
**Use case**: Organization-wide policies that apply to all Cursor workspaces.
5658

5759
## Implementation Details
@@ -81,16 +83,10 @@ fn settings_path(&self, global: bool) -> PathBuf {
8183
#### `CursorHarness::generate_hooks()`
8284

8385
```rust
84-
fn generate_hooks(&self, policy_dir: &Path, global: bool) -> Result<Value> {
85-
let policy_path = if global {
86-
// Global config - use absolute path
87-
let abs_path = fs::canonicalize(policy_dir)
88-
.unwrap_or_else(|_| policy_dir.to_path_buf());
89-
abs_path.display().to_string()
90-
} else {
91-
// Project config - use relative path from workspace root
92-
".cupcake".to_string()
93-
};
86+
fn generate_hooks(&self, _policy_dir: &Path, _global: bool) -> Result<Value> {
87+
// Project and global hooks both evaluate the workspace .cupcake;
88+
// the global config is auto-discovered by the engine independently.
89+
let policy_path = ".cupcake".to_string();
9490

9591
Ok(json!({
9692
"version": 1,
@@ -126,7 +122,8 @@ cupcake init --global --harness cursor
126122
# Verify hooks created at user location
127123
cat ~/.cursor/hooks.json
128124

129-
# Should show absolute path: --policy-dir /Users/alice/.config/cupcake
125+
# Should show project-relative path: --policy-dir .cupcake
126+
# (global config is auto-discovered by the engine, not via --policy-dir)
130127
```
131128

132129
## Comparison with Claude Code
@@ -137,12 +134,12 @@ cat ~/.cursor/hooks.json
137134
| **User hooks** | `~/.claude/settings.json`| `~/.cursor/hooks.json`|
138135
| **Hook location choice** | Respects `--global` flag | Respects `--global` flag |
139136
| **Policy path (project)** | `$CLAUDE_PROJECT_DIR/.cupcake` | `.cupcake` (relative) |
140-
| **Policy path (global)** | Absolute path | Absolute path |
137+
| **Policy path (global)** | `$CLAUDE_PROJECT_DIR/.cupcake` | `.cupcake` (relative) |
141138
| **Process cwd** | Project root (via env var) | Workspace root (direct) |
142139

143140
## Key Takeaways
144141

145142
1. **Cursor supports both project and user-level hooks** - stored at `.cursor/hooks.json` and `~/.cursor/hooks.json`
146143
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
144+
3. **Global init creates user-level hooks** - `~/.cursor/hooks.json` with the same project-relative policy path as project init; the global config is auto-discovered by the engine, not passed via `--policy-dir`
148145
4. **Cursor spawns hooks with cwd=workspace root** - enabling relative path resolution

cupcake-cli/src/harness_config.rs

Lines changed: 20 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -61,17 +61,14 @@ impl HarnessConfig for ClaudeHarness {
6161
}
6262
}
6363

64-
fn generate_hooks(&self, policy_dir: &Path, global: bool) -> Result<Value> {
65-
// Determine the policy path to use in commands
66-
let policy_path = if global {
67-
// Global config - use absolute path
68-
let abs_path =
69-
fs::canonicalize(policy_dir).unwrap_or_else(|_| policy_dir.to_path_buf());
70-
abs_path.display().to_string()
71-
} else {
72-
// Project config - use environment variable for portability
73-
"$CLAUDE_PROJECT_DIR/.cupcake".to_string()
74-
};
64+
fn generate_hooks(&self, _policy_dir: &Path, _global: bool) -> Result<Value> {
65+
// Both project and global installs evaluate the project at the harness's
66+
// working directory; org-wide global policies are auto-discovered by the
67+
// engine independently of --policy-dir. Pointing global hooks at the global
68+
// config root mis-resolved project paths and skipped local policies
69+
// (issue #104 follow-up), so global and project hooks use the same
70+
// project-relative path.
71+
let policy_path = "$CLAUDE_PROJECT_DIR/.cupcake".to_string();
7572

7673
Ok(json!({
7774
"hooks": {
@@ -132,17 +129,12 @@ impl HarnessConfig for CursorHarness {
132129
}
133130
}
134131

135-
fn generate_hooks(&self, policy_dir: &Path, global: bool) -> Result<Value> {
136-
// Determine the policy path to use in commands
137-
let policy_path = if global {
138-
// Global config - use absolute path
139-
let abs_path =
140-
fs::canonicalize(policy_dir).unwrap_or_else(|_| policy_dir.to_path_buf());
141-
abs_path.display().to_string()
142-
} else {
143-
// Project config - use relative path from workspace root
144-
".cupcake".to_string()
145-
};
132+
fn generate_hooks(&self, _policy_dir: &Path, _global: bool) -> Result<Value> {
133+
// Both project and global installs evaluate the project at Cursor's
134+
// workspace root (cwd); org-wide global policies are auto-discovered by
135+
// the engine independently of --policy-dir. See the Claude impl and
136+
// issue #104 follow-up for why global no longer uses the config root.
137+
let policy_path = ".cupcake".to_string();
146138

147139
// Cursor's hook configuration format - official hooks.json structure
148140
// Reference: https://cursor.com/docs/agent/hooks.md
@@ -194,17 +186,12 @@ impl HarnessConfig for FactoryHarness {
194186
}
195187
}
196188

197-
fn generate_hooks(&self, policy_dir: &Path, global: bool) -> Result<Value> {
198-
// Determine the policy path to use in commands
199-
let policy_path = if global {
200-
// Global config - use absolute path
201-
let abs_path =
202-
fs::canonicalize(policy_dir).unwrap_or_else(|_| policy_dir.to_path_buf());
203-
abs_path.display().to_string()
204-
} else {
205-
// Project config - use environment variable for portability
206-
"\"$FACTORY_PROJECT_DIR\"/.cupcake".to_string()
207-
};
189+
fn generate_hooks(&self, _policy_dir: &Path, _global: bool) -> Result<Value> {
190+
// Both project and global installs evaluate the project at Factory's
191+
// working directory; org-wide global policies are auto-discovered by the
192+
// engine independently of --policy-dir. See the Claude impl and issue
193+
// #104 follow-up for why global no longer uses the config root.
194+
let policy_path = "\"$FACTORY_PROJECT_DIR\"/.cupcake".to_string();
208195

209196
Ok(json!({
210197
"hooks": {

cupcake-cli/tests/harness_integration_test.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,13 +211,16 @@ fn test_init_global_with_claude_harness() {
211211
let settings_content = fs::read_to_string(&global_settings).unwrap();
212212
let settings: Value = serde_json::from_str(&settings_content).unwrap();
213213

214-
// Global should use absolute paths, not $CLAUDE_PROJECT_DIR
214+
// Global hooks evaluate the project at the harness working directory
215+
// and rely on the engine's independent auto-discovery of the global
216+
// config; they use the same project-relative path as project hooks
217+
// (issue #104 follow-up). They must NOT point at the global config root.
215218
let command = settings["hooks"]["PreToolUse"][0]["hooks"][0]["command"]
216219
.as_str()
217220
.unwrap();
218221
assert!(
219-
!command.contains("$CLAUDE_PROJECT_DIR"),
220-
"Global config should use absolute paths"
222+
command.contains("$CLAUDE_PROJECT_DIR/.cupcake"),
223+
"Global config should use the project-relative policy dir, got: {command}"
221224
);
222225
}
223226
}

0 commit comments

Comments
 (0)