@reddb-io/red-skills-dev 2.75.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +30 -0
  3. package/package.json +32 -0
  4. package/skills/engineering/README.md +36 -0
  5. package/skills/engineering/_report-runtime/WRAPPER.md +47 -0
  6. package/skills/engineering/afk/AGENT-PROMPT.md +295 -0
  7. package/skills/engineering/afk/SAFETY.md +103 -0
  8. package/skills/engineering/afk/SKILL.md +160 -0
  9. package/skills/engineering/afk/TROUBLESHOOTING.md +260 -0
  10. package/skills/engineering/afk/actions-lane.md +213 -0
  11. package/skills/engineering/afk/bin/README.md +29 -0
  12. package/skills/engineering/afk/bin/afk.mjs +51 -0
  13. package/skills/engineering/afk/defaults/cargo-pre-worktree.sh +37 -0
  14. package/skills/engineering/afk/defaults/envelope-post-attempt.sh +47 -0
  15. package/skills/engineering/afk/defaults/gradle-pre-worktree.sh +42 -0
  16. package/skills/engineering/afk/defaults/heartbeat-post-attempt.sh +43 -0
  17. package/skills/engineering/afk/defaults/validation-post-merge.sh +77 -0
  18. package/skills/engineering/afk/detectors/README.md +72 -0
  19. package/skills/engineering/afk/detectors/cargo.sh +27 -0
  20. package/skills/engineering/afk/detectors/gradle.sh +35 -0
  21. package/skills/engineering/afk/docs/BOOT-SWEEPS.md +81 -0
  22. package/skills/engineering/afk/docs/CONFIG.md +238 -0
  23. package/skills/engineering/afk/docs/ENVELOPE.md +177 -0
  24. package/skills/engineering/afk/docs/HANDOFF.md +70 -0
  25. package/skills/engineering/afk/docs/LIVENESS.md +56 -0
  26. package/skills/engineering/afk/docs/OPERATIONS.md +409 -0
  27. package/skills/engineering/afk/examples/only-mine.sh +32 -0
  28. package/skills/engineering/afk/examples/red-afk-attempt-action.yml +53 -0
  29. package/skills/engineering/afk/examples/rs-afk-attempt.yml +76 -0
  30. package/skills/engineering/afk/fleet.md +127 -0
  31. package/skills/engineering/afk/hooks/red-cargo +44 -0
  32. package/skills/engineering/afk/hooks/red-envelope +50 -0
  33. package/skills/engineering/afk/hooks/red-gradle +50 -0
  34. package/skills/engineering/afk/hooks/red-heartbeat +46 -0
  35. package/skills/engineering/afk/hooks/red-validation +79 -0
  36. package/skills/engineering/afk/monitor.md +170 -0
  37. package/skills/engineering/afk/runner-claude-minimax.md +71 -0
  38. package/skills/engineering/afk/runner-claude.md +67 -0
  39. package/skills/engineering/afk/runner-codex.md +138 -0
  40. package/skills/engineering/afk/runner-hermes.md +40 -0
  41. package/skills/engineering/afk/runner-opencode.md +142 -0
  42. package/skills/engineering/ask-red/SKILL.md +137 -0
  43. package/skills/engineering/audit-skills/SKILL.md +46 -0
  44. package/skills/engineering/code-review/SKILL.md +106 -0
  45. package/skills/engineering/context/SKILL.md +142 -0
  46. package/skills/engineering/curate/SKILL.md +133 -0
  47. package/skills/engineering/daily-review/SKILL.md +68 -0
  48. package/skills/engineering/dashboard/SKILL.md +44 -0
  49. package/skills/engineering/diagnose/SKILL.md +155 -0
  50. package/skills/engineering/diagnose/scripts/hitl-loop.template.sh +41 -0
  51. package/skills/engineering/go/SKILL.md +144 -0
  52. package/skills/engineering/go/TROUBLESHOOTING.md +81 -0
  53. package/skills/engineering/ground-truth/SKILL.md +84 -0
  54. package/skills/engineering/hitl/SKILL.md +171 -0
  55. package/skills/engineering/hitl/TROUBLESHOOTING.md +76 -0
  56. package/skills/engineering/implement/SKILL.md +69 -0
  57. package/skills/engineering/improve-codebase-architecture/DEEPENING.md +37 -0
  58. package/skills/engineering/improve-codebase-architecture/INTERFACE-DESIGN.md +44 -0
  59. package/skills/engineering/improve-codebase-architecture/LANGUAGE.md +53 -0
  60. package/skills/engineering/improve-codebase-architecture/SKILL.md +81 -0
  61. package/skills/engineering/model-tier-policy/SKILL.md +83 -0
  62. package/skills/engineering/prototype/LOGIC.md +79 -0
  63. package/skills/engineering/prototype/SKILL.md +30 -0
  64. package/skills/engineering/prototype/UI.md +112 -0
  65. package/skills/engineering/red-doctor/APPLY.md +35 -0
  66. package/skills/engineering/red-doctor/SKILL.md +172 -0
  67. package/skills/engineering/red-gains/SKILL.md +49 -0
  68. package/skills/engineering/red-setup/INTERVIEW.md +266 -0
  69. package/skills/engineering/red-setup/ISSUE-SWEEP.md +18 -0
  70. package/skills/engineering/red-setup/REFERENCE.md +35 -0
  71. package/skills/engineering/red-setup/SKILL.md +58 -0
  72. package/skills/engineering/red-setup/WORKFLOWS.md +74 -0
  73. package/skills/engineering/red-setup/WRITE-CONTRACT.md +151 -0
  74. package/skills/engineering/red-setup/config-template.yaml +174 -0
  75. package/skills/engineering/red-setup/domain.md +52 -0
  76. package/skills/engineering/red-setup/issue-tracker-github.md +88 -0
  77. package/skills/engineering/red-setup/rsp-retirement-runbook.md +35 -0
  78. package/skills/engineering/red-setup/scripts/install-runtime-shim.sh +189 -0
  79. package/skills/engineering/red-setup/triage-labels.md +176 -0
  80. package/skills/engineering/red-setup/workflows/red-issues-needs-triage.yml +36 -0
  81. package/skills/engineering/red-statusline/HOST-NOTES.md +180 -0
  82. package/skills/engineering/red-statusline/SKILL.md +58 -0
  83. package/skills/engineering/report-bug/SKILL.md +155 -0
  84. package/skills/engineering/resolving-merge-conflicts/SKILL.md +143 -0
  85. package/skills/engineering/retake/SKILL.md +184 -0
  86. package/skills/engineering/review-adrs/SKILL.md +125 -0
  87. package/skills/engineering/start/ADR-FORMAT.md +47 -0
  88. package/skills/engineering/start/CONTEXT-FORMAT.md +77 -0
  89. package/skills/engineering/start/DOC-LANDING-FINALIZER.md +9 -0
  90. package/skills/engineering/start/SKILL.md +179 -0
  91. package/skills/engineering/tdd/SKILL.md +96 -0
  92. package/skills/engineering/tdd/deep-modules.md +33 -0
  93. package/skills/engineering/tdd/interface-design.md +31 -0
  94. package/skills/engineering/tdd/mocking.md +59 -0
  95. package/skills/engineering/tdd/tests.md +85 -0
  96. package/skills/engineering/to-spec/SKILL.md +104 -0
  97. package/skills/engineering/to-tickets/SKILL.md +158 -0
  98. package/skills/engineering/triage/AGENT-BRIEF.md +207 -0
  99. package/skills/engineering/triage/OUT-OF-SCOPE.md +101 -0
  100. package/skills/engineering/triage/SKILL.md +176 -0
  101. package/skills/engineering/verify/SKILL.md +136 -0
  102. package/skills/engineering/wayfinder/SKILL.md +128 -0
  103. package/skills/engineering/zoom-out/SKILL.md +51 -0
  104. package/skills/engineering/zoom-out/scripts/graph-mode-plumbing.sh +36 -0
  105. package/skills/engineering/zoom-out/scripts/tests/contract.test.sh +168 -0
  106. package/skills/knowledge/README.md +5 -0
  107. package/skills/knowledge/research/SKILL.md +76 -0
  108. package/skills/misc/README.md +8 -0
  109. package/skills/misc/branch-lock/SKILL.md +187 -0
  110. package/skills/misc/branch-lock/scripts/branch-lock-hook.sh +113 -0
  111. package/skills/misc/branch-lock/scripts/branch-lock-session-start.sh +81 -0
  112. package/skills/misc/branch-lock/scripts/branch-lock.sh +68 -0
  113. package/skills/misc/branch-lock/scripts/lib/dev-config.sh +126 -0
  114. package/skills/misc/branch-lock/scripts/lib/git-command-classifier.sh +198 -0
  115. package/skills/misc/branch-lock/scripts/lib/lock-store.sh +67 -0
  116. package/skills/misc/branch-lock/scripts/lib/scope-resolver.sh +43 -0
  117. package/skills/misc/branch-lock/scripts/tests/branch-lock-cli.test.sh +114 -0
  118. package/skills/misc/branch-lock/scripts/tests/claude-plugin-hook.test.sh +117 -0
  119. package/skills/misc/branch-lock/scripts/tests/codex-hook.test.sh +229 -0
  120. package/skills/misc/branch-lock/scripts/tests/dev-config.test.sh +99 -0
  121. package/skills/misc/branch-lock/scripts/tests/git-command-classifier.test.sh +187 -0
  122. package/skills/misc/branch-lock/scripts/tests/lock-store.test.sh +90 -0
  123. package/skills/misc/branch-lock/scripts/tests/scope-resolver.test.sh +63 -0
  124. package/skills/misc/branch-lock/scripts/tests/session-start.test.sh +123 -0
  125. package/skills/misc/git-guardrails-claude-code/SKILL.md +105 -0
  126. package/skills/misc/git-guardrails-claude-code/scripts/block-dangerous-git.sh +231 -0
  127. package/skills/misc/git-guardrails-claude-code/scripts/tests/block-dangerous-git.test.sh +143 -0
  128. package/skills/misc/migrate-to-shoehorn/SKILL.md +96 -0
  129. package/skills/misc/setup-pre-commit/SKILL.md +94 -0
  130. package/skills/productivity/README.md +8 -0
  131. package/skills/productivity/ff/SKILL.md +116 -0
  132. package/skills/productivity/handoff/SKILL.md +37 -0
  133. package/skills/productivity/reflect/SKILL.md +18 -0
  134. package/skills/productivity/write-a-skill/SKILL.md +190 -0
  135. package/skills/productivity/write-a-skill/WRITING-STYLE.md +62 -0
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env bash
2
+ # Unit test for lib/scope-resolver.sh (issue #60).
3
+ #
4
+ # The lock enforces in the primary checkout and is exempt in registered
5
+ # .red/tmp worktree lanes plus legacy .red/tmp/work-* worktrees. Verifies both
6
+ # scope_is_worktree and its inverse scope_should_enforce across primary roots,
7
+ # worktree roots, and paths nested inside a worktree.
8
+
9
+ set -uo pipefail
10
+
11
+ HERE="$(cd "$(dirname "$0")" && pwd)"
12
+ LIB="$(dirname "$HERE")/lib/scope-resolver.sh"
13
+
14
+ # shellcheck source=../lib/scope-resolver.sh
15
+ source "$LIB"
16
+
17
+ pass=0
18
+ fail=0
19
+
20
+ expect_eq() {
21
+ local label="$1" expected="$2" actual="$3"
22
+ if [[ "$expected" == "$actual" ]]; then
23
+ echo "PASS $label"; pass=$((pass + 1))
24
+ else
25
+ printf 'FAIL %s\n expected: %q\n actual: %q\n' "$label" "$expected" "$actual"
26
+ fail=$((fail + 1))
27
+ fi
28
+ }
29
+
30
+ rc_of() { "$@"; echo $?; }
31
+
32
+ # ===========================================================================
33
+ # primary checkout => enforce (not a worktree)
34
+ # ===========================================================================
35
+ PRIMARY="/home/dev/Work/reddb.io/red-skills"
36
+ expect_eq "primary/is_worktree rc 1" "1" "$(rc_of scope_is_worktree "$PRIMARY")"
37
+ expect_eq "primary/should_enforce rc 0" "0" "$(rc_of scope_should_enforce "$PRIMARY")"
38
+
39
+ # a path that merely contains '.red/tmp' but is not a work-* worktree still enforces
40
+ NEAR="/home/dev/Work/reddb.io/red-skills/.red/tmp"
41
+ expect_eq "near/is_worktree rc 1" "1" "$(rc_of scope_is_worktree "$NEAR")"
42
+
43
+ # ===========================================================================
44
+ # registered worktree lane root => exempt
45
+ # ===========================================================================
46
+ WT="/home/dev/Work/reddb.io/red-skills/.red/tmp/worktrees/manual/fix-docs"
47
+ expect_eq "worktree/is_worktree rc 0" "0" "$(rc_of scope_is_worktree "$WT")"
48
+ expect_eq "worktree/should_enforce rc 1" "1" "$(rc_of scope_should_enforce "$WT")"
49
+
50
+ # legacy .red/tmp/work-* remains exempt during migration
51
+ WTROOT="/home/dev/Work/reddb.io/red-skills/.red/tmp/work-abc-i1"
52
+ expect_eq "worktree-root/is_worktree rc 0" "0" "$(rc_of scope_is_worktree "$WTROOT")"
53
+
54
+ # ===========================================================================
55
+ # nested path inside a worktree => still exempt
56
+ # ===========================================================================
57
+ NESTED="/home/dev/Work/reddb.io/red-skills/.red/tmp/worktrees/manual/fix-docs/plugins/dev"
58
+ expect_eq "nested/is_worktree rc 0" "0" "$(rc_of scope_is_worktree "$NESTED")"
59
+ expect_eq "nested/should_enforce rc 1" "1" "$(rc_of scope_should_enforce "$NESTED")"
60
+
61
+ echo
62
+ echo "summary: $pass passed, $fail failed"
63
+ [ "$fail" -eq 0 ]
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env bash
2
+ # Unit test for branch-lock-session-start.sh (issue #62).
3
+ #
4
+ # The SessionStart hook offers — once, at session start — to lock the agent to
5
+ # the current branch. It must:
6
+ # - emit an instruction (exit 0, JSON on stdout) in the primary checkout when
7
+ # the branch is unlocked, naming the current branch;
8
+ # - stay silent (no stdout) inside an /afk worktree (scope exemption);
9
+ # - stay silent when a lock is already present (nothing to offer);
10
+ # - stay silent on a detached HEAD (no branch to lock).
11
+ #
12
+ # Exercises the pure decision/text helpers sourced from the hook script, plus an
13
+ # end-to-end run of the hook against throwaway git repos.
14
+
15
+ set -uo pipefail
16
+
17
+ HERE="$(cd "$(dirname "$0")" && pwd)"
18
+ HOOK="$(dirname "$HERE")/branch-lock-session-start.sh"
19
+
20
+ # Source the script for its pure helpers (the script guards main on direct run).
21
+ # shellcheck source=../branch-lock-session-start.sh
22
+ source "$HOOK"
23
+
24
+ pass=0
25
+ fail=0
26
+
27
+ expect_eq() {
28
+ local label="$1" expected="$2" actual="$3"
29
+ if [[ "$expected" == "$actual" ]]; then
30
+ echo "PASS $label"; pass=$((pass + 1))
31
+ else
32
+ printf 'FAIL %s\n expected: %q\n actual: %q\n' "$label" "$expected" "$actual"
33
+ fail=$((fail + 1))
34
+ fi
35
+ }
36
+
37
+ expect_contains() {
38
+ local label="$1" needle="$2" haystack="$3"
39
+ if [[ "$haystack" == *"$needle"* ]]; then
40
+ echo "PASS $label"; pass=$((pass + 1))
41
+ else
42
+ printf 'FAIL %s\n missing: %q\n in: %q\n' "$label" "$needle" "$haystack"
43
+ fail=$((fail + 1))
44
+ fi
45
+ }
46
+
47
+ rc_of() { "$@"; echo $?; }
48
+
49
+ # ===========================================================================
50
+ # session_start_should_prompt <root> <lockfile> <branch>
51
+ # prompt only when: primary checkout AND unlocked AND a real branch.
52
+ # ===========================================================================
53
+ PRIMARY="/home/dev/Work/reddb.io/red-skills"
54
+ WT="/home/dev/Work/reddb.io/red-skills/.red/tmp/work-abc-i1/worktree"
55
+ ABSENT_LOCK="$(mktemp -u)" # path that does not exist => unlocked
56
+
57
+ expect_eq "primary+unlocked+branch => prompt (rc 0)" \
58
+ "0" "$(rc_of session_start_should_prompt "$PRIMARY" "$ABSENT_LOCK" "main")"
59
+
60
+ expect_eq "worktree => no prompt (rc 1)" \
61
+ "1" "$(rc_of session_start_should_prompt "$WT" "$ABSENT_LOCK" "main")"
62
+
63
+ expect_eq "detached HEAD => no prompt (rc 1)" \
64
+ "1" "$(rc_of session_start_should_prompt "$PRIMARY" "$ABSENT_LOCK" "HEAD")"
65
+
66
+ expect_eq "empty branch => no prompt (rc 1)" \
67
+ "1" "$(rc_of session_start_should_prompt "$PRIMARY" "$ABSENT_LOCK" "")"
68
+
69
+ PRESENT_LOCK="$(mktemp)"; printf 'main\n' > "$PRESENT_LOCK"
70
+ expect_eq "already locked => no prompt (rc 1)" \
71
+ "1" "$(rc_of session_start_should_prompt "$PRIMARY" "$PRESENT_LOCK" "feature/x")"
72
+ rm -f "$PRESENT_LOCK"
73
+
74
+ # ===========================================================================
75
+ # session_start_prompt_text <branch> — names the branch, instructs to ask.
76
+ # ===========================================================================
77
+ TEXT="$(session_start_prompt_text "feature/login")"
78
+ expect_contains "prompt text names the branch" "feature/login" "$TEXT"
79
+ expect_contains "prompt text says to ask the user" "ask" "$TEXT"
80
+ expect_contains "prompt text points at /branch-lock" "/branch-lock" "$TEXT"
81
+
82
+ # ===========================================================================
83
+ # End-to-end: run the hook against throwaway repos.
84
+ # ===========================================================================
85
+ mk_repo() {
86
+ local dir="$1"
87
+ git init -q "$dir"
88
+ git -C "$dir" config user.email t@t.t
89
+ git -C "$dir" config user.name t
90
+ git -C "$dir" commit -q --allow-empty -m init
91
+ }
92
+
93
+ run_hook() { CLAUDE_PROJECT_DIR="$1" bash "$HOOK" <<<'{"hook_event_name":"SessionStart"}'; }
94
+
95
+ # primary checkout, unlocked => emits JSON with additionalContext naming branch
96
+ TMP_PRIMARY="$(mktemp -d)/red-skills"
97
+ mk_repo "$TMP_PRIMARY"
98
+ git -C "$TMP_PRIMARY" branch -m work-branch
99
+ OUT="$(run_hook "$TMP_PRIMARY")"
100
+ expect_contains "e2e primary: emits SessionStart event" "SessionStart" "$OUT"
101
+ expect_contains "e2e primary: names the branch" "work-branch" "$OUT"
102
+ expect_contains "e2e primary: valid additionalContext" "additionalContext" "$OUT"
103
+ if command -v jq >/dev/null 2>&1; then
104
+ CTX="$(jq -r '.hookSpecificOutput.additionalContext' <<<"$OUT" 2>/dev/null)"
105
+ expect_contains "e2e primary: JSON parses, branch in context" "work-branch" "$CTX"
106
+ fi
107
+
108
+ # already locked => silent (empty stdout)
109
+ mkdir -p "$TMP_PRIMARY/.red/tmp"
110
+ printf 'work-branch\n' > "$TMP_PRIMARY/.red/tmp/branch-lock.yaml"
111
+ OUT_LOCKED="$(run_hook "$TMP_PRIMARY")"
112
+ expect_eq "e2e locked: silent" "" "$OUT_LOCKED"
113
+
114
+ # worktree => silent even when unlocked
115
+ TMP_WT="$(mktemp -d)/red-skills/.red/tmp/work-zzz-i1/worktree"
116
+ mkdir -p "$(dirname "$TMP_WT")"
117
+ mk_repo "$TMP_WT"
118
+ OUT_WT="$(run_hook "$TMP_WT")"
119
+ expect_eq "e2e worktree: silent" "" "$OUT_WT"
120
+
121
+ echo
122
+ echo "summary: $pass passed, $fail failed"
123
+ [ "$fail" -eq 0 ]
@@ -0,0 +1,105 @@
1
+ ---
2
+ name: git-guardrails-claude-code
3
+ description: Set up Claude Code hooks to block dangerous git commands (push, reset --hard, clean, branch -D, etc.) before they execute. Use when user wants to prevent destructive git operations, add git safety hooks, or block git push/reset in Claude Code.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # Setup Git Guardrails
8
+
9
+ **Block destructive git ops before they run.**
10
+
11
+ Sets up a PreToolUse hook that intercepts and blocks dangerous git commands before Claude executes them.
12
+
13
+ <what-to-do>
14
+
15
+ ## 1. Ask scope
16
+
17
+ Ask the user: install for **this project only** (`.claude/settings.json`) or **all projects** (`~/.claude/settings.json`)?
18
+
19
+ ## 2. Copy the hook script
20
+
21
+ The bundled script is at: [scripts/block-dangerous-git.sh](scripts/block-dangerous-git.sh)
22
+
23
+ Copy it to the target location based on scope, then make it executable with `chmod +x`:
24
+
25
+ - **Project**: `.claude/hooks/block-dangerous-git.sh`
26
+ - **Global**: `~/.claude/hooks/block-dangerous-git.sh`
27
+
28
+ ## 3. Add hook to settings
29
+
30
+ Add the `PreToolUse` hook to the appropriate settings file. The block is identical for both scopes — only the `command` path differs: **project** uses `"$CLAUDE_PROJECT_DIR"/.claude/hooks/block-dangerous-git.sh`, **global** uses `~/.claude/hooks/block-dangerous-git.sh`.
31
+
32
+ ```json
33
+ {
34
+ "hooks": {
35
+ "PreToolUse": [
36
+ {
37
+ "matcher": "Bash",
38
+ "hooks": [
39
+ {
40
+ "type": "command",
41
+ "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-dangerous-git.sh"
42
+ }
43
+ ]
44
+ }
45
+ ]
46
+ }
47
+ }
48
+ ```
49
+
50
+ If the settings file already exists, merge the hook into the existing `hooks.PreToolUse` array — don't overwrite other settings.
51
+
52
+ ## 4. Ask about customization
53
+
54
+ Ask if the user wants to add or remove any patterns from the blocked list. Edit the copied script accordingly.
55
+
56
+ ## 5. Verify
57
+
58
+ Run a quick test:
59
+
60
+ ```bash
61
+ echo '{"tool_input":{"command":"git push origin main"}}' | <path-to-script>
62
+ ```
63
+
64
+ Should exit with code 2 and print a BLOCKED message to stderr.
65
+
66
+ </what-to-do>
67
+
68
+ <supporting-info>
69
+
70
+ ## What gets blocked
71
+
72
+ - `git push` (all variants including `--force`)
73
+ - `git reset --hard`
74
+ - `git clean -f` / `git clean -fd`
75
+ - `git branch -D`
76
+ - `git checkout .` / `git restore .`
77
+
78
+ When blocked, Claude sees a message telling it that it does not have authority to access these commands.
79
+
80
+ ## Primary branch guard
81
+
82
+ Once the dev plugin is enabled (`plugins.dev.enabled: true`), this hook blocks the
83
+ agent from switching the primary checkout's branch (`git switch <branch>`,
84
+ `git checkout <branch>`, `git switch -b <new>`) **unconditionally** — with no
85
+ branch-lock file and no config toggle (ADR 0083 §2, untouchable primary). `git
86
+ commit`, `git worktree add`, read-only git, and registered `.red/tmp/worktrees/<lane>/<slug>` worktrees stay
87
+ allowed; the human terminal is not intercepted. The historical
88
+ `dev.lock.primary-branch` key is retained as read-only legacy history only — it no
89
+ longer enables or disables the guard.
90
+
91
+ ## Branch-lock awareness
92
+
93
+ When an opt-in branch lock is active, this hook also blocks the branch-leaving /
94
+ work-loss family in the primary checkout. That behaviour is documented in full at
95
+ [`branch-lock`](../branch-lock/SKILL.md); this hook is self-contained and reaches
96
+ the same verdict without sourcing it.
97
+
98
+ ## Tests
99
+
100
+ The full behaviour (dangerous patterns + branch-lock awareness + worktree
101
+ scope + the no-dependency contract) is pinned by
102
+ [scripts/tests/block-dangerous-git.test.sh](scripts/tests/block-dangerous-git.test.sh),
103
+ run directly with `bash`.
104
+
105
+ </supporting-info>
@@ -0,0 +1,231 @@
1
+ #!/bin/bash
2
+ # block-dangerous-git.sh — git-guardrails PreToolUse(Bash) hook.
3
+ #
4
+ # Two independent layers, both ending in exit 2 (Claude Code's "deny this tool
5
+ # call") with a message on stderr:
6
+ #
7
+ # 1. Always-on dangerous patterns — push, reset --hard, clean -f, branch -D,
8
+ # whole-tree checkout/restore. These fire regardless of any branch lock and
9
+ # are the skill's original purpose.
10
+ #
11
+ # 2. Branch-lock awareness — when a branch lock is active in the primary
12
+ # checkout (an opt-in `.red/tmp/branch-lock.yaml` whose content is the
13
+ # locked branch), the hook ALSO blocks the branch-leaving / work-loss family
14
+ # (switch/checkout to another branch, `switch -`, bare `git stash`). This is
15
+ # a self-contained re-implementation: git-guardrails reads the lock and
16
+ # classifies the command on its own and never sources or requires the
17
+ # branch-lock skill (issue #65 — independent, no dependency). If branch-lock
18
+ # is also installed the two hooks reach the same verdict and stack
19
+ # idempotently — both deny, neither conflicts.
20
+ #
21
+ # The lock layer stays silent (no block) when there is no lock file, and is
22
+ # scope-exempt inside registered worktree lanes under .red/tmp/worktrees/,
23
+ # mirroring the branch-lock hook so the autonomous loop is never strangled.
24
+ #
25
+ # 3. Primary branch guard — when `.red/config.yaml` sets
26
+ # `dev.lock.primary-branch: true`, the hook blocks branch-switching in the
27
+ # primary checkout even without a branch-lock file. This is also
28
+ # self-contained and keeps the same worktree exemption.
29
+
30
+ INPUT=$(cat)
31
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
32
+
33
+ DANGEROUS_PATTERNS=(
34
+ "git push"
35
+ "git reset --hard"
36
+ "git clean -fd"
37
+ "git clean -f"
38
+ "git branch -D"
39
+ "git checkout \."
40
+ "git restore \."
41
+ "push --force"
42
+ "reset --hard"
43
+ )
44
+
45
+ for pattern in "${DANGEROUS_PATTERNS[@]}"; do
46
+ if echo "$COMMAND" | grep -qE "$pattern"; then
47
+ echo "BLOCKED: '$COMMAND' matches dangerous pattern '$pattern'. The user has prevented you from doing this." >&2
48
+ exit 2
49
+ fi
50
+ done
51
+
52
+ # --- Branch-lock awareness (self-contained; no branch-lock skill dependency) ---
53
+
54
+ [ -z "$COMMAND" ] && exit 0
55
+
56
+ # Project root: prefer the harness-provided dir, fall back to the git toplevel.
57
+ ROOT="${CLAUDE_PROJECT_DIR:-}"
58
+ if [ -z "$ROOT" ]; then
59
+ ROOT=$(git rev-parse --show-toplevel 2>/dev/null || true)
60
+ fi
61
+ [ -z "$ROOT" ] && exit 0
62
+
63
+ # Scope: registered worktree lanes under .red/tmp/worktrees/ are exempt even when locked.
64
+ case "$ROOT" in
65
+ */.red/tmp/worktrees/*/*) exit 0 ;;
66
+ */.red/tmp/worktrees/*/*/*) exit 0 ;;
67
+ */.red/tmp/work-*) exit 0 ;;
68
+ */.red/tmp/work-*/*) exit 0 ;;
69
+ esac
70
+
71
+ # Runtime reader for the one dev flag ADR 0043 needs. Missing file/key or
72
+ # malformed input is disabled, so the plugin-level hook is dormant by default.
73
+ _dev_lock_primary_branch_enabled() {
74
+ local _file="$1"
75
+ [ -f "$_file" ] || return 1
76
+
77
+ local -a _stack=()
78
+ local -a _indents=()
79
+ local _raw _line _indent_str _indent _rest _key _value _full _i
80
+
81
+ while IFS= read -r _raw || [ -n "$_raw" ]; do
82
+ _raw="${_raw%$'\r'}"
83
+ _line="$_raw"
84
+ if [[ "$_line" != *\"* && "$_line" != *\'* ]]; then
85
+ _line="${_line%%#*}"
86
+ fi
87
+ _line="${_line%"${_line##*[![:space:]]}"}"
88
+ [ -z "${_line//[[:space:]]/}" ] && continue
89
+
90
+ _indent_str="${_line%%[![:space:]]*}"
91
+ _indent=${#_indent_str}
92
+ (( _indent % 2 == 0 )) || return 1
93
+ _rest="${_line:$_indent}"
94
+
95
+ [[ "$_rest" =~ ^[A-Za-z_][A-Za-z0-9_-]*: ]] || return 1
96
+ _key="${_rest%%:*}"
97
+ _value="${_rest#*:}"
98
+ _value="${_value#"${_value%%[![:space:]]*}"}"
99
+
100
+ while ((${#_indents[@]} > 0 && _indents[${#_indents[@]} - 1] >= _indent)); do
101
+ local _last_stack=$(( ${#_stack[@]} - 1 ))
102
+ local _last_indent=$(( ${#_indents[@]} - 1 ))
103
+ unset "_stack[$_last_stack]"
104
+ unset "_indents[$_last_indent]"
105
+ done
106
+
107
+ _full=""
108
+ for ((_i = 0; _i < ${#_stack[@]}; _i++)); do
109
+ [ -n "$_full" ] && _full+="."
110
+ _full+="${_stack[$_i]}"
111
+ done
112
+ [ -n "$_full" ] && _full+="."
113
+ _full+="$_key"
114
+
115
+ if [ -z "$_value" ]; then
116
+ _stack+=("$_key")
117
+ _indents+=("$_indent")
118
+ continue
119
+ fi
120
+
121
+ if [[ "$_value" == \"*\" && "$_value" == *\" ]]; then
122
+ _value="${_value:1:${#_value}-2}"
123
+ elif [[ "$_value" == \'*\' && "$_value" == *\' ]]; then
124
+ _value="${_value:1:${#_value}-2}"
125
+ fi
126
+
127
+ if [ "$_full" = "dev.lock.primary-branch" ] || [ "$_full" = "plugins.dev.lock.primary-branch" ]; then
128
+ [ "$_value" = "true" ] && return 0
129
+ fi
130
+ done < "$_file"
131
+
132
+ return 1
133
+ }
134
+
135
+ _primary_branch_switch_verdict() {
136
+ local _cmd="$1"
137
+ read -ra _pb_toks <<<"$_cmd"
138
+ local _pb_n=${#_pb_toks[@]} _pb_i _pb_j
139
+ for ((_pb_i = 0; _pb_i < _pb_n; _pb_i++)); do
140
+ [ "${_pb_toks[_pb_i]}" = "git" ] || continue
141
+ local _pb_sub="${_pb_toks[_pb_i + 1]:-}"
142
+ case "$_pb_sub" in
143
+ worktree)
144
+ echo "allow"; return 0 ;;
145
+ checkout|switch)
146
+ local _pb_target="" _pb_sawdd=0
147
+ for ((_pb_j = _pb_i + 2; _pb_j < _pb_n; _pb_j++)); do
148
+ local _pb_t="${_pb_toks[_pb_j]}"
149
+ if [ "$_pb_t" = "--" ]; then _pb_sawdd=1; continue; fi
150
+ if [ "$_pb_t" = "-" ]; then _pb_target="-"; break; fi
151
+ case "$_pb_t" in -*) continue ;; esac
152
+ _pb_target="$_pb_t"; break
153
+ done
154
+ [ "$_pb_sawdd" -eq 1 ] && { echo "allow"; return 0; }
155
+ [ -z "$_pb_target" ] && { echo "allow"; return 0; }
156
+ [ "$_pb_target" = "." ] && { echo "allow"; return 0; }
157
+ echo "block"; return 0 ;;
158
+ esac
159
+ done
160
+ echo "allow"; return 0
161
+ }
162
+
163
+ if _dev_lock_primary_branch_enabled "$ROOT/.red/config.yaml" &&
164
+ [ "$(_primary_branch_switch_verdict "$COMMAND")" = "block" ]; then
165
+ cat >&2 <<EOF
166
+ BLOCKED by primary branch guard: dev.lock.primary-branch is true.
167
+ The command '$COMMAND' would switch the agent's primary checkout branch.
168
+
169
+ Allowed in the primary checkout: git commit, git worktree add, read-only git,
170
+ and non-branch-changing commands. To work on another branch, create/use a
171
+ worktree under .red/tmp/worktrees/manual/<slug> or ask the user to change the primary branch.
172
+ EOF
173
+ exit 2
174
+ fi
175
+
176
+ # Read the lock target (first line, trailing whitespace stripped). Absent or
177
+ # empty file means unlocked — nothing more to enforce.
178
+ LOCKFILE="$ROOT/.red/tmp/branch-lock.yaml"
179
+ [ -s "$LOCKFILE" ] || exit 0
180
+ IFS= read -r LOCK_BRANCH < "$LOCKFILE"
181
+ LOCK_BRANCH="${LOCK_BRANCH%"${LOCK_BRANCH##*[![:space:]]}"}"
182
+ [ -z "$LOCK_BRANCH" ] && exit 0
183
+
184
+ # Classify the command against the lock. Scans the token stream for a `git` token
185
+ # followed by a recognised subcommand, so compound commands (`cd x && git switch
186
+ # y`) are still caught. Only the branch-leaving / work-loss family blocks; the
187
+ # work-loss members already caught by the dangerous patterns above are harmless
188
+ # duplicates here.
189
+ read -ra _toks <<<"$COMMAND"
190
+ _n=${#_toks[@]}
191
+ _verdict="allow"
192
+ for ((_i = 0; _i < _n; _i++)); do
193
+ [ "${_toks[_i]}" = "git" ] || continue
194
+ _sub="${_toks[_i + 1]:-}"
195
+ case "$_sub" in
196
+ worktree)
197
+ _verdict="allow"; break ;;
198
+ stash)
199
+ _op="${_toks[_i + 2]:-}"
200
+ if [ -z "$_op" ] || [ "$_op" = "push" ] || [ "$_op" = "save" ]; then
201
+ _verdict="block"
202
+ fi
203
+ break ;;
204
+ checkout|switch)
205
+ _target=""; _sawdd=0
206
+ for ((_j = _i + 2; _j < _n; _j++)); do
207
+ _t="${_toks[_j]}"
208
+ if [ "$_t" = "--" ]; then _sawdd=1; continue; fi
209
+ if [ "$_t" = "-" ]; then _target="-"; break; fi # `switch -` = previous branch
210
+ case "$_t" in -*) continue ;; esac # skip flags (-b, -c, --track, …)
211
+ _target="$_t"; break
212
+ done
213
+ if [ "$_target" = "." ]; then _verdict="block"; break; fi # whole-tree restore (already a pattern)
214
+ [ "$_sawdd" -eq 1 ] && break # file restore: allow
215
+ [ -z "$_target" ] && break # bare checkout: allow
216
+ [ "$_target" = "$LOCK_BRANCH" ] && break # back to the lock target: allow
217
+ _verdict="block"; break ;;
218
+ esac
219
+ done
220
+
221
+ if [ "$_verdict" = "block" ]; then
222
+ cat >&2 <<EOF
223
+ BLOCKED by branch lock: this session is locked to '$LOCK_BRANCH'.
224
+ The command '$COMMAND' would switch the agent away from the locked branch or
225
+ shelve the working tree. To change or release the lock, ask the user — they
226
+ drive it with '/branch-lock <branch>' or '/branch-lock clear'.
227
+ EOF
228
+ exit 2
229
+ fi
230
+
231
+ exit 0
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env bash
2
+ # Unit test for block-dangerous-git.sh — the git-guardrails PreToolUse(Bash) hook
3
+ # (issue #65: make git-guardrails lock-aware, independent, no dependency).
4
+ #
5
+ # Two responsibilities, exercised end-to-end against throwaway repos:
6
+ # 1. The always-on dangerous-pattern block (push, reset --hard, …) is
7
+ # unchanged and lock-independent.
8
+ # 2. When a branch lock is active in the primary checkout, the hook also blocks
9
+ # the branch-leaving / work-loss family — the same verdicts the branch-lock
10
+ # hook reaches — so git-guardrails alone enforces the lock (AC1). With both
11
+ # hooks installed the overlap is idempotent: both deny, never conflict (AC2).
12
+ # And the script depends on nothing from the branch-lock skill (AC3).
13
+
14
+ set -uo pipefail
15
+
16
+ HERE="$(cd "$(dirname "$0")" && pwd)"
17
+ SCRIPTS="$(dirname "$HERE")"
18
+ HOOK="$SCRIPTS/block-dangerous-git.sh"
19
+
20
+ pass=0
21
+ fail=0
22
+
23
+ expect_eq() {
24
+ local label="$1" expected="$2" actual="$3"
25
+ if [[ "$expected" == "$actual" ]]; then
26
+ echo "PASS $label"; pass=$((pass + 1))
27
+ else
28
+ printf 'FAIL %s\n expected: %q\n actual: %q\n' "$label" "$expected" "$actual"
29
+ fail=$((fail + 1))
30
+ fi
31
+ }
32
+
33
+ # A throwaway repo, optionally locked to <branch> via the gitignored lockfile.
34
+ mk_repo() {
35
+ local dir="$1" lock="${2:-}"
36
+ git init -q -b main "$dir"
37
+ git -C "$dir" config user.email t@t.t
38
+ git -C "$dir" config user.name t
39
+ git -C "$dir" commit -q --allow-empty -m init
40
+ git -C "$dir" branch feature
41
+ if [[ -n "$lock" ]]; then
42
+ mkdir -p "$dir/.red/tmp"
43
+ printf '%s\n' "$lock" > "$dir/.red/tmp/branch-lock.yaml"
44
+ fi
45
+ }
46
+
47
+ # Run the hook against a Bash command with the project root pinned; echo exit code.
48
+ run_hook() {
49
+ local root="$1" cmd="$2"
50
+ ( cd "$root" && CLAUDE_PROJECT_DIR="$root" bash "$HOOK" \
51
+ <<<"{\"tool_input\":{\"command\":\"$cmd\"}}" ) >/dev/null 2>&1
52
+ echo $?
53
+ }
54
+
55
+ # ===========================================================================
56
+ # Always-on dangerous patterns — unchanged, lock-independent.
57
+ # ===========================================================================
58
+ RU="$(mktemp -d)/red-skills"; mk_repo "$RU" # unlocked
59
+ expect_eq "guardrail/push blocked (unlocked)" "2" "$(run_hook "$RU" "git push origin main")"
60
+ expect_eq "guardrail/reset --hard blocked" "2" "$(run_hook "$RU" "git reset --hard")"
61
+ expect_eq "guardrail/clean -fd blocked" "2" "$(run_hook "$RU" "git clean -fd")"
62
+ expect_eq "guardrail/branch -D blocked" "2" "$(run_hook "$RU" "git branch -D feature")"
63
+ expect_eq "guardrail/checkout dot blocked" "2" "$(run_hook "$RU" "git checkout .")"
64
+ expect_eq "guardrail/status allowed" "0" "$(run_hook "$RU" "git status")"
65
+ # Branch switching is NOT a guardrail pattern: allowed when no lock is active.
66
+ expect_eq "no-lock/switch other allowed" "0" "$(run_hook "$RU" "git switch feature")"
67
+ expect_eq "no-lock/checkout -b allowed" "0" "$(run_hook "$RU" "git checkout -b shiny")"
68
+ expect_eq "no-lock/stash allowed" "0" "$(run_hook "$RU" "git stash")"
69
+
70
+ # ===========================================================================
71
+ # Primary branch guard — config flag on, no branch-lock file required.
72
+ # ===========================================================================
73
+ mkdir -p "$RU/.red"
74
+ cat > "$RU/.red/config.yaml" <<'EOF'
75
+ dev:
76
+ lock:
77
+ primary-branch: true
78
+ EOF
79
+ expect_eq "primary-guard/switch other blocked" "2" "$(run_hook "$RU" "git switch feature")"
80
+ expect_eq "primary-guard/checkout other blocked" "2" "$(run_hook "$RU" "git checkout feature")"
81
+ expect_eq "primary-guard/switch -b blocked" "2" "$(run_hook "$RU" "git switch -b shiny")"
82
+ expect_eq "primary-guard/commit allowed" "0" "$(run_hook "$RU" "git commit -m wip")"
83
+ expect_eq "primary-guard/worktree add allowed" "0" "$(run_hook "$RU" "git worktree add ../wt feature")"
84
+ expect_eq "primary-guard/status allowed" "0" "$(run_hook "$RU" "git status")"
85
+
86
+ cat > "$RU/.red/config.yaml" <<'EOF'
87
+ dev:
88
+ lock:
89
+ primary-branch: false
90
+ EOF
91
+ expect_eq "primary-guard/flag off allows switch" "0" "$(run_hook "$RU" "git switch feature")"
92
+
93
+ # ===========================================================================
94
+ # AC1 — lock active in the primary checkout: branch-leaving / work-loss blocked.
95
+ # ===========================================================================
96
+ RL="$(mktemp -d)/red-skills"; mk_repo "$RL" main # locked to main
97
+ expect_eq "AC1/switch other blocked" "2" "$(run_hook "$RL" "git switch feature")"
98
+ expect_eq "AC1/checkout other blocked" "2" "$(run_hook "$RL" "git checkout feature")"
99
+ expect_eq "AC1/checkout -b new blocked" "2" "$(run_hook "$RL" "git checkout -b shiny")"
100
+ expect_eq "AC1/switch - blocked" "2" "$(run_hook "$RL" "git switch -")"
101
+ expect_eq "AC1/stash blocked" "2" "$(run_hook "$RL" "git stash")"
102
+ # Same-branch / legitimate ops still allowed while locked.
103
+ expect_eq "AC1/switch back to lock allowed" "0" "$(run_hook "$RL" "git switch main")"
104
+ expect_eq "AC1/checkout back to lock allowed" "0" "$(run_hook "$RL" "git checkout main")"
105
+ expect_eq "AC1/file restore allowed" "0" "$(run_hook "$RL" "git checkout -- src/app.ts")"
106
+ expect_eq "AC1/status allowed while locked" "0" "$(run_hook "$RL" "git status")"
107
+ expect_eq "AC1/worktree add allowed" "0" "$(run_hook "$RL" "git worktree add ../wt feature")"
108
+ # The guardrail patterns still fire while locked (idempotent with the work-loss family).
109
+ expect_eq "AC1/push blocked while locked" "2" "$(run_hook "$RL" "git push")"
110
+ expect_eq "AC1/reset --hard blocked while locked" "2" "$(run_hook "$RL" "git reset --hard")"
111
+
112
+ # ===========================================================================
113
+ # Scope — registered worktree lanes under .red/tmp/worktrees/ are exempt even with a lock.
114
+ # ===========================================================================
115
+ WT="$(mktemp -d)/red-skills/.red/tmp/worktrees/manual/fix-docs"
116
+ mkdir -p "$(dirname "$WT")"
117
+ mk_repo "$WT" main # lockfile present, but it's a worktree path
118
+ expect_eq "scope/worktree switch other allowed" "0" "$(run_hook "$WT" "git switch feature")"
119
+ mkdir -p "$WT/.red"
120
+ cat > "$WT/.red/config.yaml" <<'EOF'
121
+ dev:
122
+ lock:
123
+ primary-branch: true
124
+ EOF
125
+ expect_eq "scope/worktree primary guard allowed" "0" "$(run_hook "$WT" "git switch feature")"
126
+ # …but a worktree's own dangerous patterns still fire (guardrail is scope-independent).
127
+ expect_eq "scope/worktree push still blocked" "2" "$(run_hook "$WT" "git push")"
128
+
129
+ # ===========================================================================
130
+ # AC3 — no dependency on the branch-lock skill: the script must not source or
131
+ # require any of its files. (Naming the skill in a comment is fine — what would
132
+ # create a dependency is sourcing/dot-including its modules or reaching into its
133
+ # directory.)
134
+ # ===========================================================================
135
+ if grep -qE '(source|^\s*\.)[[:space:]].*(lock-store|scope-resolver|git-command-classifier|branch-lock/)' "$HOOK"; then
136
+ echo "FAIL AC3/no branch-lock dependency (sources a branch-lock module)"; fail=$((fail + 1))
137
+ else
138
+ echo "PASS AC3/no branch-lock dependency"; pass=$((pass + 1))
139
+ fi
140
+
141
+ echo
142
+ echo "summary: $pass passed, $fail failed"
143
+ [ "$fail" -eq 0 ]