feat(hooks): Cure 4b cross-cutting hooks + 1.22.0 (DOJ-4571)#32
Conversation
|
@dojo-code-reviewer review |
There was a problem hiding this comment.
Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.
There was a problem hiding this comment.
✅ Approved
Approved — 0 blockers, 2 P2, 1 P3. Confidence: 3/5.
🟡 P2 — Major
hooks/cross-cutting/pre-write-cross-repo-schema-ownership.sh:144— 🟡 P2 (major) — Thesed/Iflag (case-insensitive substitution) is a GNU extension and is not supported by BSDsedon macOS. This will cause syntax errors and crash the hook on developer macOS machines.
We can achieve portable case-insensitive matching by explicitly using case-insensitive characters in the sed expression instead of relying on the /I flag.
| sed -E 's/^[A-Za-z]+[[:space:]]+[A-Za-z]+[[:space:]]+(([iI][fF][[:space:]]+([nN][oO][tT][[:space:]]+)?[eE][xX][iI][sS][tT][sS][[:space:]]+)?)//' \
hooks/cross-cutting/pre-write-no-cleartext-secret-in-config.sh:81— 🟡 P2 (major) — The multiple backslashes in double quotes (HIGH_IMPACT_RE="\\\\\\$\\\\{...") make the regex highly prone to platform-specific interpretation issues and extremely difficult to maintain. Using single quotes in Bash prevents shell-expansion of backslashes and ensures the regex is passed togrepexactly as written.
HIGH_IMPACT_RE='\$\{[A-Z_]*('"${TAILS}"')[A-Z0-9_]*\}'
🔵 P3 — Minor / nit
hooks/cross-cutting/pre-write-no-cleartext-secret-in-config.sh:105— 🔵 P3 (minor) — Clean up theCURE_REstring using single quotes to match the portable, shell-escape-free pattern.
CURE_RE='('"${CURE_SUFFIXES}"')\}$'
Total findings: 2 compliance, 1 business context (3 total)
| fi | ||
|
|
||
| # Capture identifiers after CREATE TABLE, ALTER TABLE, DROP TABLE, RENAME TABLE. | ||
| # Strip optional IF NOT EXISTS / IF EXISTS, optional schema prefix (`public.`), |
There was a problem hiding this comment.
🟡 P2 (major) — The sed /I flag (case-insensitive substitution) is a GNU extension and is not supported by BSD sed on macOS. This will cause syntax errors and crash the hook on developer macOS machines.
We can achieve portable case-insensitive matching by explicitly using case-insensitive characters in the sed expression instead of relying on the /I flag.
| # Strip optional IF NOT EXISTS / IF EXISTS, optional schema prefix (`public.`), | |
| | sed -E 's/^[A-Za-z]+[[:space:]]+[A-Za-z]+[[:space:]]+(([iI][fF][[:space:]]+([nN][oO][tT][[:space:]]+)?[eE][xX][iI][sS][tT][sS][[:space:]]+)?)//' \ |
| # ─── Built-in high-impact secret patterns ─────────────────────────────────── | ||
| # Tail patterns (must appear after an optional [A-Z_]* prefix and before an | ||
| # optional [A-Z0-9_]* suffix). Mirror the DOJ-4554 PR #266 review fixes for | ||
| # numbered/versioned secrets: ${DB_PASSWORD_1}, ${JWT_SECRET_V2}, etc. |
There was a problem hiding this comment.
🟡 P2 (major) — The multiple backslashes in double quotes (HIGH_IMPACT_RE="\\\\\\$\\\\{...") make the regex highly prone to platform-specific interpretation issues and extremely difficult to maintain. Using single quotes in Bash prevents shell-expansion of backslashes and ensures the regex is passed to grep exactly as written.
| # numbered/versioned secrets: ${DB_PASSWORD_1}, ${JWT_SECRET_V2}, etc. | |
| HIGH_IMPACT_RE='\$\{[A-Z_]*('"${TAILS}"')[A-Z0-9_]*\}' |
| fi | ||
|
|
||
| CURE_SUFFIXES="$BUILTIN_SUFFIXES" | ||
| if [[ -n "$EXTRA_SUFFIXES_PIPE" ]]; then |
There was a problem hiding this comment.
🔵 P3 (minor) — Clean up the CURE_RE string using single quotes to match the portable, shell-escape-free pattern.
| if [[ -n "$EXTRA_SUFFIXES_PIPE" ]]; then | |
| CURE_RE='('"${CURE_SUFFIXES}"')\}$' |
…-3946 2026-05-14 taxonomy Version bump: - package.json, .claude-plugin/plugin.json, README.md → 1.21.0 - 1.20.0 reserved for DOJ-4571 PR #32 (Cure 4b cross-repo hooks) CHANGELOG: - Add [1.21.0] entry describing recovered atomic-design hooks - Add back-reference on [1.14.0] entry noting the source landed in 1.21.0 Pillar taxonomy fix (references/atomic-design-rules.example.json): - Drop 'course' pillar (absorbed into 'pathways' per 2026-05-14 audit) - Fix canonical_folders: 'courses' and 'course' both → 'pathways' (was incorrectly mapping 'courses' → 'course' singular) Verified against project_pillar_ownership.md memory (9 pillars established post-DOJ-3946 audit 2026-05-14). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…14.0 code) (#28) * feat(hooks): recover atomic-design ownership-drift hooks (code for the 1.14.0 entry) The atomic-design enforcement hooks documented in CHANGELOG [1.14.0] were never committed — the code was stranded in a local stash (2026-05-14); only the CHANGELOG/version landed on main. This recovers it onto its own branch, applied cleanly onto current main (1.19.0): - hooks/atomic/{pre-atomic.sh, post-atomic-drift.sh, README.md} - commands/atomic-rules-init.md, references/atomic-design-rules.example.json, schemas/atomic-design-rules.schema.json - wiring in hooks/hooks.json, hooks/pre-edit.sh, hooks/lib/parse-input.sh - skills/spec-recommend additions (SKILL.md + anti-examples-block.md) Version left at main's 1.19.0 (no bump) pending a decision: ship as a fix fulfilling [1.14.0], or cut a new release. 225/225 hook tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(release): bump 1.19.0 → 1.21.0 + align example pillars with DOJ-3946 2026-05-14 taxonomy Version bump: - package.json, .claude-plugin/plugin.json, README.md → 1.21.0 - 1.20.0 reserved for DOJ-4571 PR #32 (Cure 4b cross-repo hooks) CHANGELOG: - Add [1.21.0] entry describing recovered atomic-design hooks - Add back-reference on [1.14.0] entry noting the source landed in 1.21.0 Pillar taxonomy fix (references/atomic-design-rules.example.json): - Drop 'course' pillar (absorbed into 'pathways' per 2026-05-14 audit) - Fix canonical_folders: 'courses' and 'course' both → 'pathways' (was incorrectly mapping 'courses' → 'course' singular) Verified against project_pillar_ownership.md memory (9 pillars established post-DOJ-3946 audit 2026-05-14). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Addressed all 3 review findings in commit a2709b1:
Tests still 248/248 passing. No behavior change. @dojo-code-reviewer review |
Address dojo-code-reviewer findings on PR #32 (2 P2 + 1 P3, 0 blockers). P2 — schema_ownership hook crashes on BSD sed (macOS): pre-write-cross-repo-schema-ownership.sh used `sed -E '...//I'` to match the TABLE keyword case-insensitively while stripping the IF NOT EXISTS clause. The `/I` flag is a GNU extension; BSD sed on macOS errors out with "unknown option to \`s'". Replaced with explicit bracket-class spelling (`[Tt][Aa][Bb][Ll][Ee]` etc.) which is ERE-portable across both implementations. `grep -i` upstream stays as-is — it's portable. P2 + P3 — cleartext-secret hook regex quoting: HIGH_IMPACT_RE + CURE_RE used quad-backslash escaping inside double quotes ("\\\\\\$\\\\{..."), which is fragile across shell versions and hard to maintain. Switched to single-quote-plus-interpolation convention: the static literal part stays single-quoted (no bash re-interpretation), the dynamic ${TAILS}/${CURE_SUFFIXES} group is interpolated via a separate double-quoted segment. Significantly more readable, matches the rest of the toolkit's bash codebase. Tests: 248/248 still passing (23 cross-cutting + 225 manifest). No behavior change — these are pure portability and readability fixes. Refs: DOJ-4571, PR #32 review by dojo-code-reviewer. Created by Claude Code on behalf of @lapc506 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
✅ Approved
Approved — 0 blockers, 1 P2, 3 P3. Confidence: 3/5.
🟡 P2 — Major
hooks/cross-cutting/pre-write-no-cleartext-secret-in-config.sh:125— 🟡 P2 (major) — TheALLextraction regex is\$\{[A-Z][A-Z0-9_]*\}which mandates that the first character after{must be a capital letter ([A-Z]). However, standard environment variable names can start with an underscore (e.g.${_JWT_SECRET}or${_PRIVATE_KEY}). If a user writes a secret starting with_, it will matchHIGH_IMPACT_RE(which allows[A-Z_]*prefixes), but will fail to be extracted intoALL, resulting in a silent bypass of the hook.
Changing [A-Z] to [A-Z_] in the ALL extraction regex ensures that all valid uppercase/underscore environment variables are correctly extracted and analyzed.
ALL=$(echo "$PROPOSED" | grep -oE '\$\{[A-Z_][A-Z0-9_]*\}')
🔵 P3 — Minor / nit
hooks/cross-cutting/lib/jq-input.sh:93— 🔵 P3 (minor) — The comment on line 91 references "the shell single-quoted regex", but the actual command on line 93 uses shell double quotes. Since it's double-quoted, backslashes are parsed by Bash first before being sent togrep, resulting in complex escaping like[[:space:]\\\\\"].
Using a single-quoted regex around the static portion avoids double-quote shell interpolation and simplifies the escape sequences.
printf '%s' "$HOOK_INPUT_RAW" | grep -qE '(#|//|--)[[:space:]]*hook-bypass:[[:space:]]*'"${marker}"'([[:space:]\\"]|$)'
hooks/cross-cutting/pre-write-version-bump-discipline.sh:57— 🔵 P3 (minor) — If theversion_bumpskey is absent in the config JSON, evaluating.version_bumps | lengthraises ajqerror ("null has no length") and exits non-zero. Although this is safely caught by your fallbackENTRIES_COUNT=${ENTRIES_COUNT:-0}and2>/dev/null, we can prevent thejqengine from throwing an error entirely by defaulting the value to an empty array before checking its length.
ENTRIES_COUNT=$(printf '%s' "$CONFIG_JSON" | jq -r ".${SURFACE} // [] | length" 2>/dev/null)
hooks/cross-cutting/pre-write-version-bump-discipline.sh:96— 🔵 P3 (minor) — Since this script is run under#!/bin/bash, you can leverage pure Bash arithmetic for loops instead of invoking the externalsequtility. This is cleaner, avoids subprocess overhead, and avoids any portability concerns with differentseqimplementations (especially whenENTRIES_COUNTis 0).
for ((i=0; i<ENTRIES_COUNT; i++)); do
Total findings: 1 compliance, 3 business context (4 total)
| ALL=$(echo "$PROPOSED" | grep -oE '\$\{[A-Z][A-Z0-9_]*\}') | ||
| FILTERED="" | ||
| while IFS= read -r tok; do | ||
| [[ -z "$tok" ]] && continue |
There was a problem hiding this comment.
🟡 P2 (major) — The ALL extraction regex is \$\{[A-Z][A-Z0-9_]*\} which mandates that the first character after { must be a capital letter ([A-Z]). However, standard environment variable names can start with an underscore (e.g. ${_JWT_SECRET} or ${_PRIVATE_KEY}). If a user writes a secret starting with _, it will match HIGH_IMPACT_RE (which allows [A-Z_]* prefixes), but will fail to be extracted into ALL, resulting in a silent bypass of the hook.
Changing [A-Z] to [A-Z_] in the ALL extraction regex ensures that all valid uppercase/underscore environment variables are correctly extracted and analyzed.
| [[ -z "$tok" ]] && continue | |
| ALL=$(echo "$PROPOSED" | grep -oE '\$\{[A-Z_][A-Z0-9_]*\}') |
| [[ -z "$marker" ]] && return 1 | ||
| cc_read_stdin | ||
| # ERE quoting note: backslash inside a bracket class needs to be escaped | ||
| # to a literal "\" in the shell single-quoted regex. |
There was a problem hiding this comment.
🔵 P3 (minor) — The comment on line 91 references "the shell single-quoted regex", but the actual command on line 93 uses shell double quotes. Since it's double-quoted, backslashes are parsed by Bash first before being sent to grep, resulting in complex escaping like [[:space:]\\\\\"].
Using a single-quoted regex around the static portion avoids double-quote shell interpolation and simplifies the escape sequences.
| # to a literal "\" in the shell single-quoted regex. | |
| printf '%s' "$HOOK_INPUT_RAW" | grep -qE '(#|//|--)[[:space:]]*hook-bypass:[[:space:]]*'"${marker}"'([[:space:]\\"]|$)' |
| fi | ||
|
|
||
| cc_load_config || exit 0 | ||
| [[ "$CONFIG_FOUND" != "1" ]] && exit 0 |
There was a problem hiding this comment.
🔵 P3 (minor) — If the version_bumps key is absent in the config JSON, evaluating .version_bumps | length raises a jq error ("null has no length") and exits non-zero. Although this is safely caught by your fallback ENTRIES_COUNT=${ENTRIES_COUNT:-0} and 2>/dev/null, we can prevent the jq engine from throwing an error entirely by defaulting the value to an empty array before checking its length.
| [[ "$CONFIG_FOUND" != "1" ]] && exit 0 | |
| ENTRIES_COUNT=$(printf '%s' "$CONFIG_JSON" | jq -r ".${SURFACE} // [] | length" 2>/dev/null) |
| # Match basename or trailing-component glob. | ||
| case "$FILE_PATH" in | ||
| */"$fp"|"$fp"|*"$fp") | ||
| MATCH_IDX="$i" |
There was a problem hiding this comment.
🔵 P3 (minor) — Since this script is run under #!/bin/bash, you can leverage pure Bash arithmetic for loops instead of invoking the external seq utility. This is cleaner, avoids subprocess overhead, and avoids any portability concerns with different seq implementations (especially when ENTRIES_COUNT is 0).
| MATCH_IDX="$i" | |
| for ((i=0; i<ENTRIES_COUNT; i++)); do |
Three generalized PreToolUse hooks distributed via the toolkit so every
consumer repo inherits cross-cutting defenses without duplicating bash
scripts. Per-repo opt-in via `.claude/config/cross-cutting-hooks.json`.
File absence -> all three hooks no-op (full backward compatibility).
Hooks (under `hooks/cross-cutting/`):
- pre-write-no-cleartext-secret-in-config.sh
Blocks Write/Edit/MultiEdit of JSON/YAML/TOML/env config files that
introduce ${...KEY|SECRET|TOKEN|PASSWORD|...} placeholders without
the cure-shape _FILE/_PATH suffix. Generalized from DOJ-4554's
openclaw.json-specific version (PR #266 in dojo-agent-openclaw-plugin).
- pre-write-cross-repo-schema-ownership.sh
Blocks new SQL migrations for tables not owned by this repo, per a
config-driven owned_tables allowlist + migration_paths glob. Empty
allowlist blocks every migration in the configured paths (the gateway
pattern). Prevents the DOJ-4524 15-day persistence freeze failure mode.
- pre-write-version-bump-discipline.sh
Blocks multi-step version bumps by delegating to a per-repo validator
script. Each entry in version_bumps[] names a file pattern, version-
extraction regex, and validator. Old version read from git HEAD blob;
new from proposed content. Bash native =~ matching (avoids sed-delim
clashes with URL-shape regexes containing /).
Per-surface defer_to_local_hook flag implements the belt-and-braces
coexistence with existing Cure 4a hooks: dojo-agent-openclaw-plugin's
4a hooks stay in place AND the repo opts in with defer_to_local_hook=
true on all three surfaces; dojo-os has no 4a coverage and opts in with
4b owning enforcement.
Three bypass-marker comment leaders (#, //, --) so the marker fits
whichever syntax the target file uses. Trailing terminator class
includes backslash so JSON-serialized embedded newlines (marker\\n)
don't break detection.
Files:
- hooks/cross-cutting/{*.sh,README.md,lib/,tests/}
- schemas/cross-cutting-hooks.schema.json — JSON Schema for the config
- hooks/hooks.json — register 3 new scripts under PreToolUse Write|Edit|MultiEdit AFTER pre-edit.sh
- hooks/test-hooks.sh — wire in test-cross-cutting.sh runner
- package.json, .claude-plugin/plugin.json, README.md — bump 1.19.0 -> 1.20.0
- package.json files[] — add schemas/ so the JSON Schema ships in the npm package
- CHANGELOG.md — [1.20.0] entry
- openspec/changes/2026-05-28-doj-4571-cure-4b-cross-repo-hooks/ —
approved proposal/design/tasks spec
Tests: 23/23 cross-cutting + 225/225 manifest rules = 248/248 total.
Consumer-repo opt-in PRs (dojo-os + dojo-agent-openclaw-plugin) land as
sibling PRs after 1.20.0 publishes. Per the DOJ-4571 belt-and-braces
decision the gateway's existing 4a hooks are NOT deleted.
Refs: DOJ-4571 (this work), DOJ-4554 (Cure 4a foundation),
DOJ-4064 (4-cure thesis), DOJ-4524, DOJ-4208, DOJ-4061.
Created by Claude Code on behalf of @lapc506
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Address dojo-code-reviewer findings on PR #32 (2 P2 + 1 P3, 0 blockers). P2 — schema_ownership hook crashes on BSD sed (macOS): pre-write-cross-repo-schema-ownership.sh used `sed -E '...//I'` to match the TABLE keyword case-insensitively while stripping the IF NOT EXISTS clause. The `/I` flag is a GNU extension; BSD sed on macOS errors out with "unknown option to \`s'". Replaced with explicit bracket-class spelling (`[Tt][Aa][Bb][Ll][Ee]` etc.) which is ERE-portable across both implementations. `grep -i` upstream stays as-is — it's portable. P2 + P3 — cleartext-secret hook regex quoting: HIGH_IMPACT_RE + CURE_RE used quad-backslash escaping inside double quotes ("\\\\\\$\\\\{..."), which is fragile across shell versions and hard to maintain. Switched to single-quote-plus-interpolation convention: the static literal part stays single-quoted (no bash re-interpretation), the dynamic ${TAILS}/${CURE_SUFFIXES} group is interpolated via a separate double-quoted segment. Significantly more readable, matches the rest of the toolkit's bash codebase. Tests: 248/248 still passing (23 cross-cutting + 225 manifest). No behavior change — these are pure portability and readability fixes. Refs: DOJ-4571, PR #32 review by dojo-code-reviewer. Created by Claude Code on behalf of @lapc506 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
a2709b1 to
4e441b8
Compare
|
Rebased onto current main (PR #28 landed as 1.21.0 ahead of this PR, bumping main's version 1.19.0 → 1.21.0). Rebase outcome:
Commit titles retain "1.20.0" since that's how they were originally authored; the PR title and all artifacts show the rebased 1.22.0. 248/248 tests still passing post-rebase. Mergeable=CLEAN once CI completes. |
|
@dojo-code-reviewer review (rebased onto 1.21.0; version is now 1.22.0; see previous comment for rebase details) |
Absorbs PR #28 (1.21.0 atomic-design ownership hooks), PR #32 (1.22.0 DOJ-4571 cross-cutting hooks), and PR #29 (gemini-review multi-model). Bumps this PR's version label from 1.20.0 → 1.23.0 to land on top of the new main. Conflict resolution: version-only on package.json, plugin.json, marketplace.json, README.md. CHANGELOG.md: renamed [1.20.0] entry to [1.23.0] (in-place), kept [1.22.0] and [1.21.0] from main intact, updated parallel-version-coordination note. Tests: 320 / 320 passing (297 manifest + 23 cross-cutting). Created by Claude Code on behalf of @lapc506
Summary
Ships Cure 4b cross-cutting PreToolUse hooks per DOJ-4571, the next layer up from the Cure 4a hooks that DOJ-4554 (PR #266 in
dojo-agent-openclaw-plugin) landed.The 4-cure thesis (DOJ-4064) has two write-time layers:
.claude/hooks/in each repo, repo-specific patterns. Shipped.make-no-mistakes-toolkitso every repo using the toolkit inherits cross-cutting defenses without duplicating bash scripts. This PR.3 of DOJ-4554's 6 hooks were generalizable; this PR ships the generic versions plus the infrastructure (per-repo config, JSON Schema, test runner) to support them.
Scope — three standalone hooks
All three live in
hooks/cross-cutting/and are registered inhooks/hooks.jsonunderPreToolUse Write|Edit|MultiEditAFTER the existingpre-edit.shdispatcher (manifest-driven rules fire first; cross-cutting hooks layer on).pre-write-no-cleartext-secret-in-config.shcleartext_secretsdojo-agent-openclaw-plugin/.claude/hooks/pre-write-no-cleartext-secret-in-openclaw-json.sh— now triggers on any JSON/YAML/TOML/env config file, not justopenclaw.jsonpre-write-cross-repo-schema-ownership.shschema_ownershippre-write-plugin-side-migration.sh— now supports bothowned_tables=[](gateway pattern: no migrations at all) andowned_tables=[…](allowlist) modespre-write-version-bump-discipline.shversion_bumpspre-write-openclaw-version-bump-discipline.sh— now content-shape-agnostic; per-repo config names file pattern + version-extraction regex + validator scriptPer-repo opt-in via
.claude/config/cross-cutting-hooks.jsonFile absence → all three hooks no-op silently (full backward compatibility for repos that haven't opted in). Per-surface
enabled: trueflag is required for any enforcement.{ "$schema": ".../schemas/cross-cutting-hooks.schema.json", "version": 1, "cleartext_secrets": { "enabled": true, "defer_to_local_hook": false, "extra_block_patterns": [], "extra_cure_suffixes": [] }, "schema_ownership": { "enabled": true, "defer_to_local_hook": false, "owned_tables": [], "migration_paths": ["supabase/migrations"] }, "version_bumps": [ { "file_pattern": "Dockerfile", "version_regex": "openclaw/releases/download/(v[0-9]+\\.[0-9]+\\.[0-9]+)/", "validator_script": "scripts/check-openclaw-version-bump.sh", "validator_args": [], "defer_to_local_hook": false } ] }JSON Schema at
schemas/cross-cutting-hooks.schema.json(added topackage.jsonfiles[]so it ships in the npm package for editor autocomplete + CI validation).Belt-and-braces with existing Cure 4a hooks
Per Andrés' Phase 0 decision, the 3 Cure 4a hooks in
dojo-agent-openclaw-pluginSTAY in place after this lands. To prevent rare divergence between the tighter 4a hook and the looser 4b hook on the same surface, each surface (and each entry insideversion_bumps[]) carries adefer_to_local_hookboolean. Whentrue:[<hook>] info: defer_to_local_hook=true; local 4a hook owns this surface in this repo) andexit 0.When
false(default), both hooks fire. They produce the same verdict by construction (4b is a direct generalization of 4a), so double-blocks are harmless; the user-visible artifact is two stderr blocks instead of one.3-layer rollback
cleartext_secrets.enabled = false(or any other top-level key) in the consumer-repo configCLAUDE_DISABLE_PLUGIN_HOOKS=1in your env1.19.0in its plugin install commandA full rollback (delete the config file) is also valid — the hooks no-op without their config.
Fail-open invariants
Every hook
exit 0(silently passes) when any of these are true:CLAUDE_DISABLE_PLUGIN_HOOKS=1jqnot on PATHversionenabledis falsedefer_to_local_hookis true (logs info-stderr)version_bumps: validator script missing/non-executableMatches the existing toolkit hook posture (defense in depth, never a single point of failure).
Tests — 248/248 passing
hooks/cross-cutting/tests/test-cross-cutting.shEach cross-cutting fixture spins up an isolated git repo in a tempdir with a synthetic config + git HEAD blob so the tests are hermetic — they never depend on the toolkit repo's own state. Coverage per hook:
cleartext_secrets(8 fixtures): blocks service-role placeholder in YAML, allows_FILEcure shape, no-op without config, no-op when disabled, honors bypass marker, defers to local 4a hook, passes non-config file types, allows custom_REFcure suffixschema_ownership(8 fixtures): blocks whenowned_tables=[], allows owned table, blocks unowned table, no-op when disabled, no-op without config, passes Edit (only Write intercepted), honors bypass marker, defers to local 4a hookversion_bumps(7 fixtures): blocks multi-minor via validator, allows single-minor, no-op when empty, no-op when validator missing, defers to local 4a hook, honors bypass marker, passes when version unchangedWired into
npm run test-hooksso CI runs both blocks together.Bugs surfaced + fixed during implementation
Worth flagging because all three are reusable across future cross-cutting hooks:
marker\n...— the literal two-char sequence, not a real newline) madecc_has_bypass_markerfail because the trailing terminator class required whitespace or end-of-string. Fixed by adding\\and"to the terminator class.(#|//)only. Extended to(#|//|--)so SQL/Haskell/Lua files can use native comment syntax for bypass markers (-- hook-bypass: cross-cutting-schema-ownershipinside a.sqlmigration).s/...{regex}.../. When the user-suppliedversion_regexcontained/(the common case for URL paths likeopenclaw/releases/download/(v[0-9]+...)/), sed failed withunknown option to 's'. Replaced sed-based extraction with bash native=~matching +${BASH_REMATCH[1]}— no delimiter issues, simpler code, identical semantics.Post-crash recovery note
The implementation session crashed mid-way (API socket closed unexpectedly) after the first test run surfaced these 3 bugs. State recovery was clean: worktree, branch, OpenSpec docs, all 3 hook scripts, lib, schema, README, and tests were intact as untracked files. Re-ran tests (confirmed 20/23 same failures), applied the 3 fixes above, re-tested to 23/23 green, then proceeded with version bump + CHANGELOG + commit + push.
Version bump 1.19.0 → 1.20.0
Minor — additive feature, zero breaking changes for repos that don't opt in. Bumped in
package.json,.claude-plugin/plugin.json,README.md. Full[1.20.0]entry inCHANGELOG.md.Consumer-rollout deferred
Per
/implementHITL protocol, the sibling consumer-repo opt-in PRs DO NOT land in this PR:dojo-osPR — config withdefer_to_local_hook: falseeverywhere (4b owns enforcement; no existing 4a coverage for these surfaces)dojo-agent-openclaw-pluginPR — config withdefer_to_local_hook: trueon all three surfaces (the existing 4a hooks own enforcement; 4b is documentation + ready-for-retirement layer)Those land as sibling PRs after this toolkit PR merges +
1.20.0publishes. Tracked inopenspec/changes/2026-05-28-doj-4571-cure-4b-cross-repo-hooks/tasks.mdand gated by Andrés via thecross-repo-rolloutHITL gate.Files changed (17, +2195 LOC)
OpenSpec change record
Per dojo-os/DOJ-4571 acceptance: full design proposal lives at
openspec/changes/2026-05-28-doj-4571-cure-4b-cross-repo-hooks/—proposal.md(Why + What),design.md(architecture decision, file layout, config schema, hook anatomy, fail-open invariants, bypass markers, versioning + rollback, test strategy),tasks.md(implementation + consumer-rollout checklist). Approved by Andrés at thespec-reviewHITL gate before any code was written.Refs
Closes DOJ-4571.
Created by Claude Code on behalf of @lapc506
🤖 Generated with Claude Code