Skip to content

chore(claude): enforce per-package openspec, block root specs#2348

Open
iobuhov wants to merge 1 commit into
mainfrom
chore/openspec-per-package-guardrail
Open

chore(claude): enforce per-package openspec, block root specs#2348
iobuhov wants to merge 1 commit into
mainfrom
chore/openspec-per-package-guardrail

Conversation

@iobuhov

@iobuhov iobuhov commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

What

Adds a guardrail so agents can no longer create OpenSpec specs/changes at the monorepo root. Specs/changes must live per-package.

Why

OpenSpec resolves its openspec/ directory relative to the current working directory and does not walk up the tree. Since agents run from the repo root, every openspec new change and every /opsx artifact write landed in the root openspec/. In a monorepo, specs should live inside the relevant package.

Changes

  • New PreToolUse hook .claude/hooks/validate-openspec-path.sh (registered in .claude/settings.json for Bash|Write|Edit|MultiEdit):
    • Blocks Write/Edit/MultiEdit into root openspec/changes/** and openspec/specs/** (the reliable backstop).
    • Blocks openspec new / init / archive unless the command cds into a packages/… dir first.
    • Allows package-level openspec/… writes, read-only commands (openspec list), and root config.yaml.
  • AGENTS.md — documents the per-package rule.
  • /opsx:new and /opsx:propose — updated to cd into the target package before running openspec.

Testing

Manually exercised the hook against 18 block/allow cases (absolute + relative paths, with and without CLAUDE_PROJECT_DIR, edge/bypass attempts) — all behave as expected. The Bash rule is a heuristic early-error; the Write/Edit backstop is the real enforcement and catches root-spec file creation regardless of how the directory was made.

🤖 Generated with Claude Code

…x docs

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@iobuhov
iobuhov requested a review from a team as a code owner July 21, 2026 11:36
@github-actions

Copy link
Copy Markdown
Contributor

AI Code Review

⚠️ Approved with suggestions — low-severity items only, safe to merge


What was reviewed

File Change
.claude/hooks/validate-openspec-path.sh New PreToolUse hook that blocks root-level OpenSpec writes and commands
.claude/settings.json Registers the new hook for Bash|Write|Edit|MultiEdit
.claude/commands/opsx/new.md Updated to require cd into target package before openspec new
.claude/commands/opsx/propose.md Same — updated to require cd before openspec new
AGENTS.md Documents the per-package OpenSpec constraint

Skipped (out of scope): dist/, pnpm-lock.yaml

All CI checks passed ✅


Findings

⚠️ Low — Bash heuristic can be bypassed with whitespace or semicolon tricks

File: .claude/hooks/validate-openspec-path.sh lines 72–80
Note: The Bash guard does a substring match on the lowercased command string. A command like openspec new change foo; cd packages/pluggableWidgets/foo-web or cd packages/x && true; openspec new would match *"cd "*"packages/"* and slip through. The PR already calls this out as "a heuristic early-error" and notes the Write/Edit backstop is the real enforcement — that backstop is sound. Worth adding a brief comment to the Bash block noting the ordering dependency (cd must precede openspec, not just co-occur) so future maintainers don't think it's watertight.


⚠️ Low — propose.md uses an unresolved placeholder PACKAGE_DIR / CHANGE_NAME

File: .claude/commands/opsx/propose.md lines 37–39
Note: The updated code block shows:

cd PACKAGE_DIR && openspec new change "CHANGE_NAME"

Whereas new.md was updated to the concrete pattern cd packages/<type>/<name-web> && openspec new change "<name>". The symbolic placeholders in propose.md are less instructive — an agent following the command literally would run cd PACKAGE_DIR which is likely to fail. Suggest aligning with new.md's style: cd packages/<type>/<name-web> && openspec new change "<name>".


Positives

  • The Write/Edit/MultiEdit backstop is the right enforcement layer — it's path-based and cannot be bypassed by command construction, which is exactly where this class of guardrail should live.
  • The field() helper using inline Python is a clean, dependency-free way to parse JSON in a bash hook; avoids requiring jq in the environment.
  • CLAUDE_PROJECT_DIR fallback chain (env var → cwd field → $PWD) is defensive and correct.
  • AGENTS.md constraint entry directly references the hook file, making the rule and its enforcement discoverable from one place.
  • The PR description is thorough: it explains the root cause, lists the 18 test cases exercised, and correctly characterises the two-layer enforcement model.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants