Skip to content

Commit e58c8b2

Browse files
iobuhovclaude
andcommitted
chore(claude): add per-package openspec guardrail hook and update opsx docs
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 122f633 commit e58c8b2

5 files changed

Lines changed: 127 additions & 6 deletions

File tree

.claude/commands/opsx/new.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,20 @@ Start a new change using the experimental artifact-driven approach.
3131

3232
**Otherwise**: Omit `--schema` to use the default.
3333

34-
3. **Create the change directory**
34+
3. **Create the change directory (inside the target package)**
35+
36+
Specs/changes are PER-PACKAGE. First determine which package this change
37+
belongs to (ask the user if unclear), then `cd` into it before running openspec.
38+
NEVER create changes at the monorepo root — a guardrail hook blocks it.
3539

3640
```bash
37-
openspec new change "<name>"
41+
cd packages/<type>/<name-web> && openspec new change "<name>"
3842
```
3943

4044
Add `--schema <name>` only if the user requested a specific workflow.
41-
This creates a scaffolded change at `openspec/changes/<name>/` with the selected schema.
45+
This creates a scaffolded change at `packages/<type>/<name-web>/openspec/changes/<name>/`
46+
with the selected schema. If the package has no `openspec/` yet, run
47+
`openspec init` inside the package first.
4248

4349
4. **Show the artifact status**
4450

.claude/commands/opsx/propose.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,19 @@ When ready to implement, run /opsx:apply
3131

3232
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
3333

34-
2. **Create the change directory**
34+
2. **Create the change directory (inside the target package)**
35+
36+
Specs/changes are PER-PACKAGE. First determine which package this change
37+
belongs to (ask the user if unclear), then `cd` into it before running openspec.
38+
NEVER create changes at the monorepo root — a guardrail hook blocks it.
3539

3640
```bash
37-
openspec new change "<name>"
41+
cd PACKAGE_DIR && openspec new change "CHANGE_NAME"
3842
```
3943

40-
This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
44+
This creates a scaffolded change at `<package-dir>/openspec/changes/<name>/`
45+
with `.openspec.yaml`. If the package has no `openspec/` yet, run
46+
`openspec init` inside the package first.
4147

4248
3. **Get the artifact build order**
4349

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#!/usr/bin/env bash
2+
# validate-openspec-path.sh — Pre-execution hook (OpenSpec location guardrail)
3+
#
4+
# Claude Code invokes this hook before Bash / Write / Edit / MultiEdit tool calls.
5+
# It enforces that OpenSpec specs and changes are created PER-PACKAGE, never at
6+
# the monorepo root. OpenSpec resolves its `openspec/` directory relative to the
7+
# current working directory, so the root `openspec/` must stay off-limits.
8+
#
9+
# Input: JSON on stdin {"tool_name":"...","tool_input":{...},"cwd":"..."}
10+
# Output: exit 0 to allow, exit 2 with JSON {"error":"..."} to block.
11+
#
12+
# IMPORTANT: Do not modify, disable, or bypass this hook.
13+
14+
set -euo pipefail
15+
16+
INPUT="$(cat)"
17+
18+
field() {
19+
# $1 = dotted path into the JSON (e.g. tool_input.file_path)
20+
printf '%s' "$INPUT" | python3 -c "
21+
import sys, json
22+
d = json.load(sys.stdin)
23+
cur = d
24+
for k in '''$1'''.split('.'):
25+
if isinstance(cur, dict):
26+
cur = cur.get(k, '')
27+
else:
28+
cur = ''
29+
print(cur if isinstance(cur, str) else '')
30+
" 2>/dev/null || true
31+
}
32+
33+
TOOL_NAME="$(field tool_name)"
34+
35+
# Resolve the repo root. Claude Code exports CLAUDE_PROJECT_DIR for hooks; fall
36+
# back to the hook's reported cwd, then to the process cwd.
37+
REPO_ROOT="${CLAUDE_PROJECT_DIR:-}"
38+
if [[ -z "$REPO_ROOT" ]]; then
39+
REPO_ROOT="$(field cwd)"
40+
fi
41+
if [[ -z "$REPO_ROOT" ]]; then
42+
REPO_ROOT="$PWD"
43+
fi
44+
REPO_ROOT="${REPO_ROOT%/}"
45+
46+
ROOT_OPENSPEC="$REPO_ROOT/openspec"
47+
48+
block() {
49+
printf '{"error":"BLOCKED by OpenSpec guardrail: %s Specs/changes are per-package. cd into the target package (e.g. packages/pluggableWidgets/<widget>) and run openspec there — never at the monorepo root."}\n' "$1" >&2
50+
exit 2
51+
}
52+
53+
case "$TOOL_NAME" in
54+
Write|Edit|MultiEdit)
55+
FILE_PATH="$(field tool_input.file_path)"
56+
[[ -z "$FILE_PATH" ]] && exit 0
57+
58+
# Normalize relative paths against the repo root.
59+
case "$FILE_PATH" in
60+
/*) ABS="$FILE_PATH" ;;
61+
*) ABS="$REPO_ROOT/$FILE_PATH" ;;
62+
esac
63+
64+
# Block writes into the ROOT openspec changes/specs trees only.
65+
case "$ABS" in
66+
"$ROOT_OPENSPEC"/changes/*|"$ROOT_OPENSPEC"/specs/*)
67+
block "cannot write '$FILE_PATH' under the root openspec/ directory."
68+
;;
69+
esac
70+
exit 0
71+
;;
72+
73+
Bash)
74+
COMMAND="$(field tool_input.command)"
75+
[[ -z "$COMMAND" ]] && exit 0
76+
77+
CMD_LOWER="$(printf '%s' "$COMMAND" | tr '[:upper:]' '[:lower:]')"
78+
79+
# Only care about spec-creating openspec subcommands.
80+
if [[ "$CMD_LOWER" == *"openspec new"* \
81+
|| "$CMD_LOWER" == *"openspec init"* \
82+
|| "$CMD_LOWER" == *"openspec archive"* ]]; then
83+
# Allow it only when the command first cd's into a package directory.
84+
if [[ "$CMD_LOWER" == *"cd "*"packages/"* ]]; then
85+
exit 0
86+
fi
87+
block "'openspec new/init/archive' must run inside a package."
88+
fi
89+
exit 0
90+
;;
91+
92+
*)
93+
exit 0
94+
;;
95+
esac

.claude/settings.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@
3131
]
3232
},
3333
"hooks": {
34+
"PreToolUse": [
35+
{
36+
"matcher": "Bash|Write|Edit|MultiEdit",
37+
"hooks": [
38+
{
39+
"type": "command",
40+
"command": ".claude/hooks/validate-openspec-path.sh"
41+
}
42+
]
43+
}
44+
],
3445
"PostToolUse": [
3546
{
3647
"matcher": "Edit|Write|MultiEdit",

AGENTS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ Monorepo of official Mendix pluggable web widgets. pnpm workspaces + Turbo.
2929
## Constraints
3030

3131
- Never modify dist/, generated files, or lockfiles
32+
- OpenSpec specs/changes are PER-PACKAGE — never create them at the monorepo root.
33+
Always `cd packages/<type>/<name>` before any `openspec new/init/archive`. The
34+
root `openspec/` directory is off-limits (enforced by `.claude/hooks/validate-openspec-path.sh`).
3235
- XML property keys: lowerCamelCase, must match TypeScript props exactly
3336
- Don't override core Atlas UI classes
3437
- Prefer tree-shakable imports for new dependencies

0 commit comments

Comments
 (0)