xtrm-tools 2.0.1 → 2.0.3

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 (29) hide show
  1. package/README.md +0 -2
  2. package/cli/dist/index.cjs +19 -19
  3. package/cli/dist/index.cjs.map +1 -1
  4. package/cli/package.json +1 -1
  5. package/hooks/README.md +107 -4
  6. package/hooks/beads-close-memory-prompt.mjs +49 -0
  7. package/hooks/beads-commit-gate.mjs +64 -0
  8. package/hooks/beads-edit-gate.mjs +72 -0
  9. package/hooks/beads-gate-utils.mjs +121 -0
  10. package/hooks/beads-stop-gate.mjs +61 -0
  11. package/hooks/main-guard.mjs +139 -0
  12. package/package.json +4 -3
  13. package/project-skills/py-quality-gate/.claude/settings.json +1 -1
  14. package/project-skills/service-skills-set/.claude/skills/creating-service-skills/SKILL.md +12 -12
  15. package/project-skills/service-skills-set/.claude/skills/creating-service-skills/references/script_quality_standards.md +13 -0
  16. package/project-skills/service-skills-set/.claude/skills/creating-service-skills/references/service_skill_system_guide.md +14 -0
  17. package/project-skills/service-skills-set/.claude/skills/creating-service-skills/scripts/__pycache__/bootstrap.cpython-314.pyc +0 -0
  18. package/project-skills/service-skills-set/.claude/skills/scoping-service-skills/SKILL.md +6 -6
  19. package/project-skills/service-skills-set/.claude/skills/updating-service-skills/SKILL.md +1 -1
  20. package/project-skills/service-skills-set/.claude/skills/using-service-skills/SKILL.md +2 -2
  21. package/project-skills/service-skills-set/.claude/skills/using-service-skills/scripts/__pycache__/skill_activator.cpython-314.pyc +0 -0
  22. package/project-skills/service-skills-set/.claude/skills/using-service-skills/scripts/__pycache__/test_skill_activator.cpython-314-pytest-9.0.2.pyc +0 -0
  23. package/project-skills/service-skills-set/.claude/skills/using-service-skills/scripts/skill_activator.py +2 -2
  24. package/project-skills/service-skills-set/.claude/skills/using-service-skills/scripts/test_skill_activator.py +58 -0
  25. package/project-skills/ts-quality-gate/.claude/settings.json +1 -1
  26. package/project-skills/main-guard/.claude/hooks/main-guard.cjs +0 -188
  27. package/project-skills/main-guard/.claude/settings.json +0 -16
  28. package/project-skills/main-guard/.claude/skills/using-main-guard/SKILL.md +0 -135
  29. package/project-skills/main-guard/README.md +0 -163
package/cli/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xtrm-cli",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "Claude Code tools installer (skills, hooks, MCP servers)",
5
5
  "main": "./dist/index.js",
6
6
  "type": "module",
package/hooks/README.md CHANGED
@@ -139,18 +139,121 @@ Hooks intercept specific events in the Claude Code lifecycle to provide:
139
139
  **Purpose**: Displays custom status line information.
140
140
  **Trigger**: StatusLine
141
141
 
142
+ ## Workflow Enforcement Hooks (JavaScript)
143
+
144
+ Installed globally to `~/.claude/hooks/` by `xtrm install`. Require Node.js.
145
+
146
+ ### main-guard.mjs
147
+
148
+ **Purpose**: Blocks direct file edits and dangerous git operations on protected branches (`main`/`master`). Enforces the feature-branch → PR workflow.
149
+
150
+ **Trigger**: PreToolUse (`Edit|Write|MultiEdit|NotebookEdit|Bash`)
151
+
152
+ **Blocks**:
153
+ - Write/Edit/MultiEdit/NotebookEdit on protected branches
154
+ - `git commit` directly on protected branches
155
+ - `git push` to protected branches
156
+
157
+ **Configuration** (injected into `~/.claude/settings.json`):
158
+ ```json
159
+ {
160
+ "hooks": {
161
+ "PreToolUse": [{
162
+ "matcher": "Edit|Write|MultiEdit|NotebookEdit|Bash",
163
+ "hooks": [{ "type": "command", "command": "~/.claude/hooks/main-guard.mjs", "timeout": 5000 }]
164
+ }]
165
+ }
166
+ }
167
+ ```
168
+
169
+ ---
170
+
171
+ ### beads-gate-utils.mjs
172
+
173
+ **Purpose**: Shared utility module imported by all beads gate hooks. Not registered as a hook itself.
174
+
175
+ **Exports**: `resolveCwd`, `isBeadsProject`, `getSessionClaim`, `getTotalWork`, `getInProgress`, `clearSessionClaim`, `withSafeBdContext`
176
+
177
+ **Requires**: `bd` (beads CLI), `dolt`
178
+
179
+ ---
180
+
181
+ ### beads-edit-gate.mjs
182
+
183
+ **Purpose**: Blocks file edits when the current session has not claimed a beads issue via `bd kv`. Prevents free-riding in multi-agent and multi-session scenarios.
184
+
185
+ **Trigger**: PreToolUse (`Edit|Write|MultiEdit|NotebookEdit|mcp__serena__*`)
186
+
187
+ **Behavior**:
188
+ - Session has claim (`bd kv get "claimed:<session_id>"`) → allow
189
+ - No claim + no trackable work → allow (clean-start state)
190
+ - No claim + open/in_progress issues exist → block
191
+ - Falls back to global in_progress check when `session_id` is absent
192
+
193
+ **Requires**: `bd`, `dolt`
194
+
195
+ ---
196
+
197
+ ### beads-commit-gate.mjs
198
+
199
+ **Purpose**: Blocks `git commit` when the current session still has an unclosed beads claim.
200
+
201
+ **Trigger**: PreToolUse (`Bash`) — only fires when command matches `git commit`
202
+
203
+ **Requires**: `bd`, `dolt`
204
+
205
+ ---
206
+
207
+ ### beads-stop-gate.mjs
208
+
209
+ **Purpose**: Blocks the agent from stopping when the current session has an unclosed beads claim.
210
+
211
+ **Trigger**: Stop
212
+
213
+ **Requires**: `bd`, `dolt`
214
+
215
+ ---
216
+
217
+ ### beads-close-memory-prompt.mjs
218
+
219
+ **Purpose**: After `bd close`, clears the session's kv claim and injects a reminder to capture knowledge before moving on.
220
+
221
+ **Trigger**: PostToolUse (`Bash`) — only fires when command matches `bd close`
222
+
223
+ **Requires**: `bd`, `dolt`
224
+
225
+ ---
226
+
227
+ ## Beads claim workflow
228
+
229
+ ```bash
230
+ # Claim an issue before editing
231
+ bd update <id> --status=in_progress
232
+ bd kv set "claimed:<session_id>" "<id>"
233
+
234
+ # Edit files freely
235
+ # ...
236
+
237
+ # Close when done — hook auto-clears the claim
238
+ bd close <id>
239
+ ```
240
+
241
+ ---
242
+
142
243
  ## Installation
143
244
 
144
- 1. Copy hooks to Claude Code directory:
245
+ Use `xtrm install` to deploy all hooks automatically. For manual setup:
246
+
247
+ 1. Copy hooks to the global Claude Code directory:
145
248
  ```bash
146
- cp hooks/* ~/.claude/hooks/
249
+ cp hooks/*.mjs hooks/*.py ~/.claude/hooks/
147
250
  ```
148
251
 
149
252
  2. Make scripts executable:
150
253
  ```bash
151
- chmod +x ~/.claude/hooks/*.py ~/.claude/hooks/*.js
254
+ chmod +x ~/.claude/hooks/*.mjs ~/.claude/hooks/*.py
152
255
  ```
153
256
 
154
- 3. Configure hooks in `~/.claude/settings.json`.
257
+ 3. Merge hook entries into `~/.claude/settings.json`.
155
258
 
156
259
  4. Restart Claude Code.
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ // beads-close-memory-prompt — Claude Code PostToolUse hook
3
+ // After `bd close`: clears session claim from bd kv, then injects a short
4
+ // reminder into Claude's context to capture knowledge and consider underused
5
+ // beads features.
6
+ // Output to stdout is shown to Claude as additional context.
7
+ //
8
+ // Installed by: xtrm install
9
+
10
+ import { readFileSync } from 'node:fs';
11
+ import {
12
+ resolveCwd, isBeadsProject, clearSessionClaim, withSafeBdContext,
13
+ } from './beads-gate-utils.mjs';
14
+
15
+ let input;
16
+ try {
17
+ input = JSON.parse(readFileSync(0, 'utf8'));
18
+ } catch {
19
+ process.exit(0);
20
+ }
21
+
22
+ if (input.tool_name !== 'Bash') process.exit(0);
23
+ if (!/\bbd\s+close\b/.test(input.tool_input?.command ?? '')) process.exit(0);
24
+
25
+ withSafeBdContext(() => {
26
+ const cwd = resolveCwd(input);
27
+ if (!isBeadsProject(cwd)) process.exit(0);
28
+
29
+ if (input.session_id) {
30
+ clearSessionClaim(input.session_id, cwd);
31
+ }
32
+
33
+ process.stdout.write(
34
+ '\n[beads] Issue(s) closed. Before moving on:\n\n' +
35
+ ' Knowledge worth keeping?\n' +
36
+ ' bd remember "key insight from this work"\n' +
37
+ ' bd memories <keyword> -- search what is already stored\n\n' +
38
+ ' Discovered related work while implementing?\n' +
39
+ ' bd create --title="..." --deps=discovered-from:<id>\n\n' +
40
+ ' Underused features to consider:\n' +
41
+ ' bd dep add <a> <b> -- link blocking relationships between issues\n' +
42
+ ' bd graph -- visualize issue dependency graph\n' +
43
+ ' bd orphans -- issues referenced in commits but still open\n' +
44
+ ' bd preflight -- PR readiness checklist before gh pr create\n' +
45
+ ' bd stale -- issues not touched recently\n'
46
+ );
47
+
48
+ process.exit(0);
49
+ });
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ // beads-commit-gate — Claude Code PreToolUse hook
3
+ // Blocks `git commit` when this session still has an unclosed claim in bd kv.
4
+ // Falls back to global in_progress check when session_id is unavailable.
5
+ // Forces: close issues first, THEN commit.
6
+ // Exit 0: allow | Exit 2: block (stderr shown to Claude)
7
+ //
8
+ // Installed by: xtrm install
9
+
10
+ import { readFileSync } from 'node:fs';
11
+ import {
12
+ resolveCwd, isBeadsProject, getSessionClaim,
13
+ getInProgress, withSafeBdContext,
14
+ } from './beads-gate-utils.mjs';
15
+
16
+ let input;
17
+ try {
18
+ input = JSON.parse(readFileSync(0, 'utf8'));
19
+ } catch {
20
+ process.exit(0);
21
+ }
22
+
23
+ if ((input.tool_name ?? '') !== 'Bash') process.exit(0);
24
+ if (!/\bgit\s+commit\b/.test(input.tool_input?.command ?? '')) process.exit(0);
25
+
26
+ const NEXT_STEPS =
27
+ ' 3. bd close <id1> <id2> ... ← you are here\n' +
28
+ ' 4. git add <files> && git commit -m "..."\n' +
29
+ ' 5. git push -u origin <feature-branch>\n' +
30
+ ' 6. gh pr create --fill && gh pr merge --squash\n' +
31
+ ' 7. git checkout master && git reset --hard origin/master\n';
32
+
33
+ withSafeBdContext(() => {
34
+ const cwd = resolveCwd(input);
35
+ if (!isBeadsProject(cwd)) process.exit(0);
36
+
37
+ const sessionId = input.session_id;
38
+
39
+ if (sessionId) {
40
+ const claimed = getSessionClaim(sessionId, cwd);
41
+ if (claimed === null) process.exit(0);
42
+ if (!claimed) process.exit(0);
43
+
44
+ const ip = getInProgress(cwd);
45
+ const summary = ip?.summary ?? ` Claimed: ${claimed}`;
46
+
47
+ process.stderr.write(
48
+ '🚫 BEADS GATE: Close open issues before committing.\n\n' +
49
+ `Open issues:\n${summary}\n\n` +
50
+ 'Next steps:\n' + NEXT_STEPS
51
+ );
52
+ process.exit(2);
53
+ } else {
54
+ const ip = getInProgress(cwd);
55
+ if (ip === null || ip.count === 0) process.exit(0);
56
+
57
+ process.stderr.write(
58
+ '🚫 BEADS GATE: Close open issues before committing.\n\n' +
59
+ `Open issues:\n${ip.summary}\n\n` +
60
+ 'Next steps:\n' + NEXT_STEPS
61
+ );
62
+ process.exit(2);
63
+ }
64
+ });
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ // beads-edit-gate — Claude Code PreToolUse hook
3
+ // Blocks file edits when this session has not claimed a beads issue via bd kv.
4
+ // Falls back to global in_progress check when session_id is unavailable.
5
+ // Only active in projects with a .beads/ directory.
6
+ // Exit 0: allow | Exit 2: block (stderr shown to Claude)
7
+ //
8
+ // Installed by: xtrm install
9
+
10
+ import { readFileSync } from 'node:fs';
11
+ import {
12
+ resolveCwd, isBeadsProject, getSessionClaim,
13
+ getTotalWork, getInProgress, withSafeBdContext,
14
+ } from './beads-gate-utils.mjs';
15
+
16
+ let input;
17
+ try {
18
+ input = JSON.parse(readFileSync(0, 'utf8'));
19
+ } catch {
20
+ process.exit(0);
21
+ }
22
+
23
+ withSafeBdContext(() => {
24
+ const cwd = resolveCwd(input);
25
+ if (!isBeadsProject(cwd)) process.exit(0);
26
+
27
+ const sessionId = input.session_id;
28
+
29
+ if (sessionId) {
30
+ const claimed = getSessionClaim(sessionId, cwd);
31
+ if (claimed === null) process.exit(0); // bd kv unavailable — fail open
32
+ if (claimed) process.exit(0); // this session has an active claim
33
+
34
+ const totalWork = getTotalWork(cwd);
35
+ if (totalWork === null) process.exit(0); // can't determine — fail open
36
+ if (totalWork === 0) process.exit(0); // nothing to track — clean-start state
37
+
38
+ process.stderr.write(
39
+ '🚫 BEADS GATE: This session has no active claim — claim an issue before editing files.\n\n' +
40
+ ' bd update <id> --status=in_progress\n' +
41
+ ` bd kv set "claimed:${sessionId}" "<id>"\n\n` +
42
+ 'Or create a new issue:\n' +
43
+ ' bd create --title="<what you\'re doing>" --type=task --priority=2\n' +
44
+ ' bd update <id> --status=in_progress\n' +
45
+ ` bd kv set "claimed:${sessionId}" "<id>"\n`
46
+ );
47
+ process.exit(2);
48
+ } else {
49
+ // Fallback: global in_progress check (non-Claude environments / no session_id).
50
+ const ip = getInProgress(cwd);
51
+ if (ip === null) process.exit(0);
52
+ if (ip.count > 0) process.exit(0);
53
+
54
+ const totalWork = getTotalWork(cwd);
55
+ if (totalWork === null || totalWork === 0) process.exit(0);
56
+
57
+ process.stderr.write(
58
+ '🚫 BEADS GATE: No active issue — create one before editing files.\n\n' +
59
+ ' bd create --title="<what you\'re doing>" --type=task --priority=2\n' +
60
+ ' bd update <id> --status=in_progress\n\n' +
61
+ 'Full workflow (do this every session):\n' +
62
+ ' 1. bd create + bd update in_progress ← you are here\n' +
63
+ ' 2. Edit files / write code\n' +
64
+ ' 3. bd close <id> close when done\n' +
65
+ ' 4. git add <files> && git commit\n' +
66
+ ' 5. git push -u origin <feature-branch>\n' +
67
+ ' 6. gh pr create --fill && gh pr merge --squash\n' +
68
+ ' 7. git checkout master && git reset --hard origin/master\n'
69
+ );
70
+ process.exit(2);
71
+ }
72
+ });
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env node
2
+ // beads-gate-utils.mjs — shared infrastructure for beads gate hooks
3
+ // Import from sibling hooks using: import { ... } from './beads-gate-utils.mjs';
4
+ // Static ES module imports resolve relative to the importing file's location,
5
+ // not CWD, so this works regardless of the project directory.
6
+
7
+ import { execSync } from 'node:child_process';
8
+ import { existsSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+
11
+ /** Resolve project cwd from hook input JSON. */
12
+ export function resolveCwd(input) {
13
+ return input.cwd ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
14
+ }
15
+
16
+ /** Return true if the directory contains a .beads project. */
17
+ export function isBeadsProject(cwd) {
18
+ return existsSync(join(cwd, '.beads'));
19
+ }
20
+
21
+ /**
22
+ * Get the claimed issue ID for a session from bd kv.
23
+ * Returns: issue ID string if claimed, '' if not set, null if bd kv unavailable.
24
+ * Note: bd kv get exits 1 for missing keys — execSync throws, so we check err.status.
25
+ */
26
+ export function getSessionClaim(sessionId, cwd) {
27
+ try {
28
+ return execSync(`bd kv get "claimed:${sessionId}"`, {
29
+ encoding: 'utf8',
30
+ cwd,
31
+ stdio: ['pipe', 'pipe', 'pipe'],
32
+ timeout: 5000,
33
+ }).trim();
34
+ } catch (err) {
35
+ if (err.status === 1) return ''; // key not found — no claim
36
+ return null; // command failed — bd kv unavailable
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Parse work counts from a bd list output string.
42
+ * Reads the "Total: N issues (X open, Y in progress)" summary line.
43
+ * Returns { open, inProgress } or null if the line is absent.
44
+ *
45
+ * This is more reliable than counting symbols or tokens: the Total line is
46
+ * a structured summary that doesn't depend on status-legend text or box-drawing
47
+ * characters, and it's present in all non-empty bd list outputs.
48
+ */
49
+ function parseCounts(output) {
50
+ const m = output.match(/Total:\s*\d+\s+issues?\s*\((\d+)\s+open,\s*(\d+)\s+in progress\)/);
51
+ if (!m) return null;
52
+ return { open: parseInt(m[1], 10), inProgress: parseInt(m[2], 10) };
53
+ }
54
+
55
+ /**
56
+ * Get in_progress issues as { count, summary }.
57
+ * Returns null if bd is unavailable.
58
+ */
59
+ export function getInProgress(cwd) {
60
+ try {
61
+ const output = execSync('bd list --status=in_progress', {
62
+ encoding: 'utf8', cwd, stdio: ['pipe', 'pipe', 'pipe'], timeout: 8000,
63
+ });
64
+ const counts = parseCounts(output);
65
+ return {
66
+ count: counts?.inProgress ?? 0,
67
+ summary: output.trim(),
68
+ };
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Count total trackable work (open + in_progress issues) using a single bd list call.
76
+ * Returns the count, or null if bd is unavailable.
77
+ */
78
+ export function getTotalWork(cwd) {
79
+ try {
80
+ // Use default status filter (non-closed) and parse Total summary.
81
+ // Repeating --status is not additive in bd CLI and can collapse to one status.
82
+ const output = execSync('bd list', {
83
+ encoding: 'utf8', cwd, stdio: ['pipe', 'pipe', 'pipe'], timeout: 8000,
84
+ });
85
+ const counts = parseCounts(output);
86
+ if (!counts) return 0; // "No issues found." — nothing to track
87
+ return counts.open + counts.inProgress;
88
+ } catch {
89
+ return null;
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Clear the session claim key from bd kv. Non-fatal — best-effort cleanup.
95
+ */
96
+ export function clearSessionClaim(sessionId, cwd) {
97
+ try {
98
+ execSync(`bd kv clear "claimed:${sessionId}"`, {
99
+ encoding: 'utf8', cwd, stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000,
100
+ });
101
+ } catch {
102
+ // non-fatal
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Option C: wrap hook body with uniform fail-open error handling.
108
+ * Any unexpected top-level throw exits 0 (allow) rather than crashing visibly.
109
+ *
110
+ * Usage:
111
+ * withSafeBdContext(() => {
112
+ * // hook logic here — call process.exit() to set exit code
113
+ * });
114
+ */
115
+ export function withSafeBdContext(fn) {
116
+ try {
117
+ fn();
118
+ } catch {
119
+ process.exit(0);
120
+ }
121
+ }
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ // beads-stop-gate — Claude Code Stop hook
3
+ // Blocks the agent from stopping when this session has an unclosed claim in bd kv.
4
+ // Falls back to global in_progress check when session_id is unavailable.
5
+ // Exit 0: allow stop | Exit 2: block stop (stderr shown to Claude)
6
+ //
7
+ // Installed by: xtrm install
8
+
9
+ import { readFileSync } from 'node:fs';
10
+ import {
11
+ resolveCwd, isBeadsProject, getSessionClaim,
12
+ getInProgress, withSafeBdContext,
13
+ } from './beads-gate-utils.mjs';
14
+
15
+ let input;
16
+ try {
17
+ input = JSON.parse(readFileSync(0, 'utf8'));
18
+ } catch {
19
+ process.exit(0);
20
+ }
21
+
22
+ const CLOSE_PROTOCOL =
23
+ ' 3. bd close <id1> <id2> ... close all in_progress issues\n' +
24
+ ' 4. git add <files> && git commit -m "..." commit your changes\n' +
25
+ ' 5. git push -u origin <feature-branch> push feature branch\n' +
26
+ ' 6. gh pr create --fill create PR\n' +
27
+ ' 7. gh pr merge --squash merge PR\n' +
28
+ ' 8. git checkout master && git reset --hard origin/master\n';
29
+
30
+ withSafeBdContext(() => {
31
+ const cwd = resolveCwd(input);
32
+ if (!isBeadsProject(cwd)) process.exit(0);
33
+
34
+ const sessionId = input.session_id;
35
+
36
+ if (sessionId) {
37
+ const claimed = getSessionClaim(sessionId, cwd);
38
+ if (claimed === null) process.exit(0); // bd kv unavailable — fail open
39
+ if (!claimed) process.exit(0); // no active claim for this session
40
+
41
+ const ip = getInProgress(cwd);
42
+ const summary = ip?.summary ?? ` Claimed: ${claimed}`;
43
+
44
+ process.stderr.write(
45
+ '🚫 BEADS STOP GATE: Unresolved issues — complete the session close protocol.\n\n' +
46
+ `Open issues:\n${summary}\n\n` +
47
+ 'Session close protocol:\n' + CLOSE_PROTOCOL
48
+ );
49
+ process.exit(2);
50
+ } else {
51
+ const ip = getInProgress(cwd);
52
+ if (ip === null || ip.count === 0) process.exit(0);
53
+
54
+ process.stderr.write(
55
+ '🚫 BEADS STOP GATE: Unresolved issues — complete the session close protocol.\n\n' +
56
+ `Open issues:\n${ip.summary}\n\n` +
57
+ 'Session close protocol:\n' + CLOSE_PROTOCOL
58
+ );
59
+ process.exit(2);
60
+ }
61
+ });
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env node
2
+ // Claude Code PreToolUse hook — block writes and direct master pushes
3
+ // Exit 0: allow | Exit 2: block (message shown to user)
4
+ //
5
+ // Installed by: xtrm install
6
+
7
+ import { execSync } from 'node:child_process';
8
+ import { readFileSync } from 'node:fs';
9
+
10
+ let branch = '';
11
+ try {
12
+ branch = execSync('git branch --show-current', {
13
+ encoding: 'utf8',
14
+ stdio: ['pipe', 'pipe', 'pipe'],
15
+ }).trim();
16
+ } catch {}
17
+
18
+ // Determine protected branches — env var override for tests and custom setups
19
+ const protectedBranches = process.env.MAIN_GUARD_PROTECTED_BRANCHES
20
+ ? process.env.MAIN_GUARD_PROTECTED_BRANCHES.split(',').map(b => b.trim()).filter(Boolean)
21
+ : ['main', 'master'];
22
+
23
+ // Not in a git repo or not on a protected branch — allow
24
+ if (!branch || !protectedBranches.includes(branch)) {
25
+ process.exit(0);
26
+ }
27
+
28
+ let input;
29
+ try {
30
+ input = JSON.parse(readFileSync(0, 'utf8'));
31
+ } catch {
32
+ process.exit(0);
33
+ }
34
+
35
+ const tool = input.tool_name ?? '';
36
+
37
+ const WRITE_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
38
+
39
+ if (WRITE_TOOLS.has(tool)) {
40
+ process.stderr.write(
41
+ `⛔ You are on '${branch}' — never edit files directly on master.\n\n` +
42
+ 'Full workflow:\n' +
43
+ ' 1. git checkout -b feature/<name> ← start here\n' +
44
+ ' 2. bd create + bd update in_progress track your work\n' +
45
+ ' 3. Edit files / write code\n' +
46
+ ' 4. bd close <id> && git add && git commit\n' +
47
+ ' 5. git push -u origin feature/<name>\n' +
48
+ ' 6. gh pr create --fill && gh pr merge --squash\n' +
49
+ ' 7. git checkout master && git reset --hard origin/master\n'
50
+ );
51
+ process.exit(2);
52
+ }
53
+
54
+ const WORKFLOW =
55
+ 'Full workflow:\n' +
56
+ ' 1. git checkout -b feature/<name> \u2190 start here\n' +
57
+ ' 2. bd create + bd update in_progress track your work\n' +
58
+ ' 3. Edit files / write code\n' +
59
+ ' 4. bd close <id> && git add && git commit\n' +
60
+ ' 5. git push -u origin feature/<name>\n' +
61
+ ' 6. gh pr create --fill && gh pr merge --squash\n' +
62
+ ' 7. git checkout master && git reset --hard origin/master\n';
63
+
64
+ if (tool === 'Bash') {
65
+ const cmd = (input.tool_input?.command ?? '').trim().replace(/\s+/g, ' ');
66
+
67
+ // Emergency override — escape hatch for power users
68
+ if (process.env.MAIN_GUARD_ALLOW_BASH === '1') {
69
+ process.exit(0);
70
+ }
71
+
72
+ // Safe allowlist — non-mutating commands + explicit branch-exit paths.
73
+ // Important: do not allow generic checkout/switch forms, which include
74
+ // mutating variants such as `git checkout -- <path>`.
75
+ const SAFE_BASH_PATTERNS = [
76
+ /^git\s+(status|log|diff|branch|show|describe|fetch|remote|config)\b/,
77
+ /^git\s+pull\b/,
78
+ /^git\s+stash\b/,
79
+ /^git\s+worktree\b/,
80
+ /^git\s+checkout\s+-b\s+\S+/,
81
+ /^git\s+switch\s+-c\s+\S+/,
82
+ /^gh\s+/,
83
+ /^bd\s+/,
84
+ ];
85
+
86
+ if (SAFE_BASH_PATTERNS.some(p => p.test(cmd))) {
87
+ process.exit(0);
88
+ }
89
+
90
+ // Specific messages for common blocked operations
91
+ if (/^git\s+commit\b/.test(cmd)) {
92
+ process.stderr.write(
93
+ `\u26D4 Don't commit directly to '${branch}' \u2014 use a feature branch.\n\n` +
94
+ WORKFLOW
95
+ );
96
+ process.exit(2);
97
+ }
98
+
99
+ if (/^git\s+push\b/.test(cmd)) {
100
+ const tokens = cmd.split(' ');
101
+ const lastToken = tokens[tokens.length - 1];
102
+ const explicitProtected = protectedBranches.some(b => lastToken === b || lastToken.endsWith(`:${b}`));
103
+ const impliedProtected = tokens.length <= 3 && protectedBranches.includes(branch);
104
+ if (explicitProtected || impliedProtected) {
105
+ process.stderr.write(
106
+ `\u26D4 Don't push directly to '${branch}' \u2014 use the PR workflow.\n\n` +
107
+ 'Next steps:\n' +
108
+ ' 5. git push -u origin <feature-branch> \u2190 push your branch\n' +
109
+ ' 6. gh pr create --fill create PR\n' +
110
+ ' gh pr merge --squash merge it\n' +
111
+ ' 7. git checkout master sync master\n' +
112
+ ' git reset --hard origin/master\n\n' +
113
+ "If you're not on a feature branch yet:\n" +
114
+ ' git checkout -b feature/<name> (then re-commit and push)\n'
115
+ );
116
+ process.exit(2);
117
+ }
118
+ // Pushing to a feature branch — allow
119
+ process.exit(0);
120
+ }
121
+
122
+ // Default deny — block everything else on protected branches
123
+ process.stderr.write(
124
+ `\u26D4 Bash is restricted on '${branch}' \u2014 use a feature branch for file writes and script execution.\n\n` +
125
+ 'Allowed on protected branches:\n' +
126
+ ' git status / log / diff / branch / fetch / pull / stash\n' +
127
+ ' git checkout -b <name> (create feature branch \u2014 the exit path)\n' +
128
+ ' git switch -c <name> (same)\n' +
129
+ ' git worktree / config\n' +
130
+ ' gh <any> (GitHub CLI)\n' +
131
+ ' bd <any> (beads issue tracking)\n\n' +
132
+ 'To run arbitrary commands:\n' +
133
+ ' 1. git checkout -b feature/<name> \u2190 move to a feature branch, or\n' +
134
+ ' 2. MAIN_GUARD_ALLOW_BASH=1 <command> (escape hatch, use sparingly)\n'
135
+ );
136
+ process.exit(2);
137
+ }
138
+
139
+ process.exit(0);
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "xtrm-tools",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "Claude Code tools installer (skills, hooks, MCP servers)",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "bin": {
8
- "xtrm": "./cli/dist/index.cjs"
8
+ "xtrm": "cli/dist/index.cjs"
9
9
  },
10
10
  "files": [
11
11
  "README.md",
@@ -43,6 +43,7 @@
43
43
  "dotenv": "^17.3.1",
44
44
  "fs-extra": "^11.2.0",
45
45
  "kleur": "^4.1.5",
46
- "prompts": "^2.4.2"
46
+ "prompts": "^2.4.2",
47
+ "xtrm-tools": "^2.0.2"
47
48
  }
48
49
  }
@@ -2,7 +2,7 @@
2
2
  "hooks": {
3
3
  "PostToolUse": [
4
4
  {
5
- "matcher": "Write|Edit",
5
+ "matcher": "Write|Edit|MultiEdit",
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",