agileflow 4.0.0-alpha.1 → 4.0.0-alpha.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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,85 @@
3
3
  All notable changes to `agileflow` v4 are documented here.
4
4
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
+ ## [4.0.0-alpha.3] — 2026-04-20
7
+
8
+ Flow audit fixes for the alpha.2 wizard + install path. Wiring/persistence
9
+ came back PASS; this patch closes the test gap and fixes the safety +
10
+ feedback gaps the audit surfaced.
11
+
12
+ ### Fixed
13
+
14
+ - **P0 test gap** (`tests/unit/config/writer.test.js`): the per-field
15
+ round-trip block tests every other config field but skipped
16
+ `behaviors`. A future PR that drops the `behaviors:` line from
17
+ `writer.js`'s payload would have shipped silently. Added explicit
18
+ mixed-shape round-trip test (not all-true, not all-false) so the
19
+ serializer-loader pair is contractually pinned.
20
+ - **P1 damage-control silent fail-open**
21
+ (`damage-control-bash.js`/`-edit.js`/`-write.js`): when
22
+ `damage-control-patterns.yaml` is missing or unreadable, all three
23
+ hooks used to `process.exit(0)` silently — guards disabled, no
24
+ signal to the user. Now emit a stderr WARNING with the error code
25
+ and the path. Repeated warnings on every Bash/Edit/Write are
26
+ intentional: they signal "fix this or disable the preset". Hooks
27
+ still fail-open (the contract is "block dangerous things, don't
28
+ block legit work just because we can't read our own config").
29
+ - **P1 missing behaviors visibility**:
30
+ - `setup --yes` console output now prints `behaviors enabled: ...`
31
+ after the plugin list, gated on `caps.hooks` so non-Claude-Code
32
+ IDEs don't see a noisy line. Listed-as-CSV in the order: any
33
+ `loadContext, babysitDefault, damageControl, preCompactState`
34
+ that are `true`.
35
+ - Interactive `prompts.outro` now includes `behaviors active: ...`
36
+ or `behaviors active: (none — no hooks will run; re-run setup to
37
+ enable)`. A user who deselected all four behaviors no longer
38
+ finishes the wizard celebrating "X plugins enabled" while
39
+ actually getting zero hooks.
40
+ - `agileflow update` console output mirrors the same pattern.
41
+ - Install spinner message changed from `Installing N plugin(s)` to
42
+ `Installing N plugin(s) — writing hooks, skills, mirrors` so
43
+ first-time users have a clearer mental model of what `.agileflow/`
44
+ will contain.
45
+ - **P2 fresh-project context-loader / pre-compact-state**: when
46
+ `docs/09-agents/status.json` is absent (brand new project), both
47
+ hooks used to silently omit the stories section. Now emit
48
+ `(no story tracker yet — docs/09-agents/status.json not found)` so
49
+ Claude knows the section was reached, not skipped due to error. Also
50
+ surfaces `(none in progress, none ready)` when status.json exists
51
+ but is empty.
52
+
53
+ ### Tests
54
+
55
+ - 305 passing (+1 from alpha.2's 304).
56
+
57
+ ## [4.0.0-alpha.2] — 2026-04-20
58
+
59
+ Curated behavior presets — first hooks ship, but never as a free-for-all.
60
+
61
+ ### Added
62
+
63
+ - **Behavior presets in `agileflow.config.json`** (`behaviors: { loadContext, babysitDefault, damageControl, preCompactState }`). Each preset maps 1:N to hooks declared in plugin manifests via a new `behavior: <key>` field. Disabling a preset excludes its hooks from the generated `.agileflow/hook-manifest.yaml` — the script literally does not run.
64
+ - **Wizard step**: `pickBehaviors()` (`src/cli/wizard/behaviors-picker.js`) — Clack multiselect with all four presets pre-checked. Only shown for IDEs that support hooks (claude-code today; cursor/windsurf/codex skip it).
65
+ - **Six behavior-gated hooks in the `core` plugin**:
66
+ - `context-loader` (SessionStart, gated by `loadContext`) — lean v4 replacement for v3's 79KB welcome banner. Prints stories, dirty files, and recent commits.
67
+ - `babysit-mentor-injector` (SessionStart, gated by `babysitDefault`) — HARD mode mentor injection. Claude defaults to the `/agileflow:babysit` mentor pattern without explicit invocation.
68
+ - `damage-control-bash` / `-edit` / `-write` (PreToolUse, gated by `damageControl`, `skipOnError: false`) — pattern-driven safety net (`damage-control-patterns.yaml`). Blocks `rm -rf /`, `dd to /dev/sda`, fork bombs, writes to `.env`/`.ssh/`, and similar.
69
+ - `pre-compact-state` (PreCompact, gated by `preCompactState`) — dumps active stories, current command, and dirty git state so they survive Claude's compaction summary.
70
+ - **Schema + loader + writer updates**: `behaviors` is now a first-class config section. `mergeConfig` deep-merges across one level so partial user overrides don't wipe defaults.
71
+ - **Aggregator behavior gating**: `buildHookManifest(orderedPlugins, behaviors)` filters hooks where `behaviors[entry.behavior] === false`. Missing keys treated as enabled (preserves intent of partial configs).
72
+ - **15 new tests** covering behaviors filtering in the aggregator, behaviors-picker pure helpers, and the install-time integration test that asserts the manifest reflects the toggle map. Total: **304 passing** (+15 from alpha.1).
73
+
74
+ ### Changed
75
+
76
+ - `installPlugins(options)` accepts a `behaviors` option threaded through to `writeAggregatedManifest`.
77
+ - `agileflow setup` now includes a behaviors step after personalization for hook-capable IDEs.
78
+ - `agileflow update` re-reads `behaviors` from the saved config so manual edits to `agileflow.config.json` propagate without re-running the wizard.
79
+ - `defaultConfig()` ships with all four behaviors enabled. Users opt **out** at the wizard, never opt in.
80
+
81
+ ### Why curated, not free configuration
82
+
83
+ Per user feedback in alpha.1: "I don't want it to be super customizable. Users would be able to customize pretty much anything." Free hook configuration (declare any event + any matcher + any script) is technically possible via the `hooks:` map, but the wizard surface is restricted to four presets. This keeps the install path predictable and prevents the v3-era anti-pattern of every plugin author shipping their own SessionStart welcome banner.
84
+
6
85
  ## [4.0.0-alpha.1] — Unreleased
7
86
 
8
87
  v4 Phase 1 skeleton. Not yet publishable.
@@ -254,6 +333,7 @@ The path from "green test suite" to "user can `npm i agileflow@alpha`" closes he
254
333
  - **Verified `npm pack --dry-run`** from `apps/cli/`: ships 52 files (bin/, src/, content/, README.md, LICENSE, CHANGELOG.md) — 65 KB tarball, 210 KB unpacked. Tests + vitest config + node_modules excluded.
255
334
 
256
335
  **To ship `4.0.0-alpha.1` now**:
336
+
257
337
  ```bash
258
338
  cd apps/cli && npm version 4.0.0-alpha.1 --no-git-tag-version
259
339
  git commit -am "release(v4): 4.0.0-alpha.1"
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Core hook: babysit-mentor-injector (SessionStart, HARD mode).
4
+ *
5
+ * Pre-loads the babysit mentor pattern into the session by printing
6
+ * its operating rules to stdout. Claude Code includes this in the
7
+ * session context, so Claude defaults to mentor behavior (smart
8
+ * AskUserQuestion at every decision point, plan mode for non-trivial
9
+ * tasks, expert delegation, task tracking, audits) without the user
10
+ * needing to type "walk me through".
11
+ *
12
+ * Disable this hook in agileflow.config.json if you want quick-edit
13
+ * mode by default — the agileflow-babysit-mentor skill still
14
+ * activates on its keyword triggers when needed.
15
+ *
16
+ * Always exits 0.
17
+ */
18
+ process.stdout.write(`## AgileFlow Mentor Mode (default-on)
19
+
20
+ This session has the babysit-mentor pattern enabled. Apply these
21
+ rules unless the user explicitly opts out for a specific request:
22
+
23
+ 1. **Smart AskUserQuestion at every decision point.** When the work
24
+ reaches a meaningful choice (which task, which approach, whether
25
+ to commit), end the response with the AskUserQuestion tool —
26
+ specific options with one marked (Recommended). Never generic
27
+ "Continue?" / "What next?".
28
+
29
+ 2. **Plan mode for non-trivial implementation.** Call EnterPlanMode
30
+ for anything more than a typo/one-liner. Explore 3–5 files,
31
+ write the plan, ExitPlanMode. Skip for trivial fixes.
32
+
33
+ 3. **Delegate complex work to domain experts.** Use the Task tool
34
+ with appropriate subagent_type (database / api / ui / testing /
35
+ security / etc.) for complex single-domain work. Use the
36
+ orchestrator for multi-domain features. Use multi-expert for
37
+ review/analysis questions.
38
+
39
+ 4. **Track progress.** TaskCreate for any task with 3+ steps.
40
+ TaskUpdate as each completes. Don't batch.
41
+
42
+ 5. **Suggest a logic audit after every implementation.** After tests
43
+ pass, present "🔍 Run logic audit on modified files" as
44
+ (Recommended). 5 analyzers catch edge cases tests miss.
45
+
46
+ 6. **Suggest a flow audit when user-flows changed.** Plans for
47
+ non-trivial features must include a "Verify flow integrity"
48
+ step. After tests pass on flow-touching code, suggest
49
+ "🔄 Run flow audit" before commit.
50
+
51
+ To disable mentor mode entirely: edit agileflow.config.json,
52
+ set hooks.babysit-mentor-injector.enabled to false, run
53
+ \`agileflow update\`.
54
+ `);
55
+ process.exit(0);
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Core hook: context-loader (SessionStart).
4
+ *
5
+ * Prints a compact project snapshot to stdout so Claude Code includes
6
+ * it in the session prompt. Lean v4 implementation (~200 lines, vs.
7
+ * v3's 79KB welcome banner). Output is pure text — no ANSI colors.
8
+ *
9
+ * Sections (ordered by load-bearing-ness for Claude):
10
+ * 1. Project header (cwd, agileflow version, ide)
11
+ * 2. Active story / epic (from docs/09-agents/status.json if present)
12
+ * 3. Ready stories (top 5 by priority)
13
+ * 4. Git state (branch, dirty files)
14
+ * 5. Recent commits (last 5)
15
+ * 6. Hook health (last 3 entries from hook-execution.jsonl)
16
+ *
17
+ * Fail-open: any read error becomes a one-line "(unavailable)" note.
18
+ * The hook always exits 0; orchestrator-side `skipOnError: true` is
19
+ * defensive belt-and-suspenders.
20
+ */
21
+ const fs = require("fs");
22
+ const path = require("path");
23
+ const { execSync } = require("child_process");
24
+
25
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
26
+ const agileflowDir = path.join(projectDir, ".agileflow");
27
+
28
+ /** Read JSON file, returning null on any error. */
29
+ function readJSON(p) {
30
+ try {
31
+ return JSON.parse(fs.readFileSync(p, "utf8"));
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ /** Run a git command; return its stdout trimmed, or null on error. */
38
+ function git(args) {
39
+ try {
40
+ return execSync(`git ${args}`, {
41
+ cwd: projectDir,
42
+ encoding: "utf8",
43
+ stdio: ["ignore", "pipe", "ignore"],
44
+ }).trim();
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ /** Tail the last N JSONL entries from a file. */
51
+ function tailJSONL(p, n) {
52
+ try {
53
+ const lines = fs.readFileSync(p, "utf8").trim().split("\n");
54
+ return lines
55
+ .slice(-n)
56
+ .map((l) => {
57
+ try {
58
+ return JSON.parse(l);
59
+ } catch {
60
+ return null;
61
+ }
62
+ })
63
+ .filter(Boolean);
64
+ } catch {
65
+ return [];
66
+ }
67
+ }
68
+
69
+ const lines = [];
70
+ const out = (s = "") => lines.push(s);
71
+
72
+ out("## AgileFlow project context");
73
+ out("");
74
+
75
+ // 1. Project header
76
+ const config = readJSON(path.join(projectDir, "agileflow.config.json")) || {};
77
+ const enabledPlugins = Object.entries(config.plugins || {})
78
+ .filter(([, v]) => v && v.enabled)
79
+ .map(([id]) => id);
80
+ out(`cwd: ${projectDir}`);
81
+ out(`ide: ${(config.ide && config.ide.primary) || "claude-code"}`);
82
+ out(`plugins: ${enabledPlugins.join(", ") || "(none)"}`);
83
+ out(
84
+ `tone: ${(config.personalization && config.personalization.tone) || "concise"}`,
85
+ );
86
+ out("");
87
+
88
+ // 2. Active story / epic
89
+ const status = readJSON(path.join(projectDir, "docs/09-agents/status.json"));
90
+ if (!status) {
91
+ // Fresh project — no story tracker yet. Tell Claude explicitly so it
92
+ // knows the section was reached, not silently skipped due to error.
93
+ out("## Stories");
94
+ out(" (no story tracker yet — docs/09-agents/status.json not found)");
95
+ out("");
96
+ } else if (status.stories) {
97
+ const inProgress = Object.entries(status.stories)
98
+ .filter(([, s]) => s && s.status === "in_progress")
99
+ .map(([id, s]) => ({ id, ...s }));
100
+ if (inProgress.length) {
101
+ out("## In progress");
102
+ for (const s of inProgress) {
103
+ out(
104
+ ` ${s.id} ${s.title || ""} (owner: ${s.owner || "?"}, est: ${s.estimate || "?"})`,
105
+ );
106
+ }
107
+ out("");
108
+ }
109
+
110
+ // 3. Ready stories — top 5 by priority then estimate
111
+ const ready = Object.entries(status.stories)
112
+ .filter(([, s]) => s && s.status === "ready")
113
+ .map(([id, s]) => ({ id, ...s }))
114
+ .sort((a, b) => {
115
+ const pa = parseInt(String(a.priority || "P9").replace("P", ""), 10);
116
+ const pb = parseInt(String(b.priority || "P9").replace("P", ""), 10);
117
+ if (pa !== pb) return pa - pb;
118
+ return (a.estimate || 99) - (b.estimate || 99);
119
+ })
120
+ .slice(0, 5);
121
+ if (ready.length) {
122
+ out("## Ready (top 5)");
123
+ for (const s of ready) {
124
+ out(
125
+ ` ${s.id} [${s.priority || "P?"} · ${s.estimate || "?"}pts] ${s.title || ""}`,
126
+ );
127
+ }
128
+ out("");
129
+ }
130
+
131
+ if (!inProgress.length && !ready.length) {
132
+ out("## Stories");
133
+ out(" (none in progress, none ready)");
134
+ out("");
135
+ }
136
+ }
137
+
138
+ // 4. Git state
139
+ const branch = git("rev-parse --abbrev-ref HEAD");
140
+ const dirty = git("status --short");
141
+ if (branch) {
142
+ out("## Git");
143
+ out(` branch: ${branch}`);
144
+ if (dirty) {
145
+ const lines2 = dirty.split("\n").slice(0, 10);
146
+ out(
147
+ ` changes: ${lines2.length} file(s)${dirty.split("\n").length > 10 ? " (showing first 10)" : ""}`,
148
+ );
149
+ for (const l of lines2) out(` ${l}`);
150
+ } else {
151
+ out(" changes: (clean)");
152
+ }
153
+ out("");
154
+ }
155
+
156
+ // 5. Recent commits
157
+ const log = git("log --oneline -5");
158
+ if (log) {
159
+ out("## Recent commits");
160
+ for (const l of log.split("\n")) out(` ${l}`);
161
+ out("");
162
+ }
163
+
164
+ // 6. Hook health
165
+ const hookLog = tailJSONL(
166
+ path.join(agileflowDir, "logs/hook-execution.jsonl"),
167
+ 3,
168
+ );
169
+ if (hookLog.length) {
170
+ out("## Recent hook runs");
171
+ for (const e of hookLog) {
172
+ const status = e.status === "ok" ? "OK" : e.status.toUpperCase();
173
+ out(
174
+ ` ${e.timestamp || "?"} [${status}] ${e.event}/${e.hookId} (${e.durationMs || 0}ms)`,
175
+ );
176
+ }
177
+ out("");
178
+ }
179
+
180
+ process.stdout.write(lines.join("\n") + "\n");
181
+ process.exit(0);
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Core hook: damage-control-bash (PreToolUse, matcher: Bash).
4
+ *
5
+ * Reads damage-control-patterns.yaml and rejects Bash commands that
6
+ * match an `error`-severity pattern of kind `bash`. `warn`-severity
7
+ * patterns log but do not block.
8
+ *
9
+ * Stdin payload (Claude Code-shaped):
10
+ * { tool_name: "Bash", tool_input: { command: "...", description: "..." } }
11
+ *
12
+ * Exit codes:
13
+ * 0 allow (default)
14
+ * 2 block (Claude Code surfaces stderr to the user)
15
+ */
16
+ const fs = require("fs");
17
+ const path = require("path");
18
+ const yaml = require("js-yaml");
19
+
20
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
21
+ const patternsPath = path.join(
22
+ projectDir,
23
+ ".agileflow/plugins/core/hooks/damage-control-patterns.yaml",
24
+ );
25
+
26
+ async function readStdin() {
27
+ const chunks = [];
28
+ for await (const c of process.stdin) chunks.push(c);
29
+ return Buffer.concat(chunks).toString("utf8");
30
+ }
31
+
32
+ async function main() {
33
+ const raw = await readStdin();
34
+ let payload;
35
+ try {
36
+ payload = JSON.parse(raw);
37
+ } catch {
38
+ process.exit(0); // No payload, nothing to gate.
39
+ }
40
+ const command =
41
+ payload &&
42
+ payload.tool_input &&
43
+ typeof payload.tool_input.command === "string"
44
+ ? payload.tool_input.command
45
+ : "";
46
+ if (!command) process.exit(0);
47
+
48
+ let patterns = [];
49
+ try {
50
+ const parsed = yaml.load(fs.readFileSync(patternsPath, "utf8"));
51
+ patterns = Array.isArray(parsed && parsed.patterns) ? parsed.patterns : [];
52
+ } catch (err) {
53
+ // Fail open, but warn loudly: a missing/unreadable patterns file
54
+ // means every dangerous command will go through unblocked, and
55
+ // the user MUST notice. Repeated warnings on every Bash call are
56
+ // intentional — they signal "damageControl is broken, fix it or
57
+ // disable the preset in agileflow.config.json".
58
+ process.stderr.write(
59
+ `agileflow damage-control: WARNING — patterns file unreadable (${err.code || err.name}: ${patternsPath}). Bash safety guards are DISABLED until this is fixed.\n`,
60
+ );
61
+ process.exit(0);
62
+ }
63
+
64
+ for (const p of patterns) {
65
+ if (p.kind !== "bash") continue;
66
+ let re;
67
+ try {
68
+ re = new RegExp(p.regex, "i");
69
+ } catch {
70
+ continue;
71
+ }
72
+ if (re.test(command)) {
73
+ if (p.severity === "error") {
74
+ process.stderr.write(
75
+ `agileflow damage-control: BLOCKED — ${p.reason}\n pattern: ${p.regex}\n command: ${command.slice(0, 200)}\n`,
76
+ );
77
+ process.exit(2);
78
+ }
79
+ // severity: warn — log, do not block
80
+ process.stderr.write(`agileflow damage-control: WARN — ${p.reason}\n`);
81
+ }
82
+ }
83
+ process.exit(0);
84
+ }
85
+
86
+ main().catch(() => process.exit(0));
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Core hook: damage-control-edit (PreToolUse, matcher: Edit).
4
+ *
5
+ * Reads damage-control-patterns.yaml and rejects Edit operations whose
6
+ * file_path matches an `error`-severity pattern of kind `edit`.
7
+ *
8
+ * Stdin payload:
9
+ * { tool_name: "Edit", tool_input: { file_path: "...", old_string, new_string } }
10
+ *
11
+ * Exit codes: 0 allow, 2 block.
12
+ */
13
+ const fs = require("fs");
14
+ const path = require("path");
15
+ const yaml = require("js-yaml");
16
+
17
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
18
+ const patternsPath = path.join(
19
+ projectDir,
20
+ ".agileflow/plugins/core/hooks/damage-control-patterns.yaml",
21
+ );
22
+
23
+ async function readStdin() {
24
+ const chunks = [];
25
+ for await (const c of process.stdin) chunks.push(c);
26
+ return Buffer.concat(chunks).toString("utf8");
27
+ }
28
+
29
+ async function main() {
30
+ const raw = await readStdin();
31
+ let payload;
32
+ try {
33
+ payload = JSON.parse(raw);
34
+ } catch {
35
+ process.exit(0);
36
+ }
37
+ const filePath =
38
+ payload &&
39
+ payload.tool_input &&
40
+ typeof payload.tool_input.file_path === "string"
41
+ ? payload.tool_input.file_path
42
+ : "";
43
+ if (!filePath) process.exit(0);
44
+
45
+ let patterns = [];
46
+ try {
47
+ const parsed = yaml.load(fs.readFileSync(patternsPath, "utf8"));
48
+ patterns = Array.isArray(parsed && parsed.patterns) ? parsed.patterns : [];
49
+ } catch (err) {
50
+ process.stderr.write(
51
+ `agileflow damage-control: WARNING — patterns file unreadable (${err.code || err.name}: ${patternsPath}). Edit safety guards are DISABLED until this is fixed.\n`,
52
+ );
53
+ process.exit(0);
54
+ }
55
+
56
+ for (const p of patterns) {
57
+ if (p.kind !== "edit") continue;
58
+ let re;
59
+ try {
60
+ re = new RegExp(p.regex, "i");
61
+ } catch {
62
+ continue;
63
+ }
64
+ if (re.test(filePath)) {
65
+ if (p.severity === "error") {
66
+ process.stderr.write(
67
+ `agileflow damage-control: BLOCKED edit — ${p.reason}\n path: ${filePath}\n`,
68
+ );
69
+ process.exit(2);
70
+ }
71
+ process.stderr.write(
72
+ `agileflow damage-control: WARN — ${p.reason} (path: ${filePath})\n`,
73
+ );
74
+ }
75
+ }
76
+ process.exit(0);
77
+ }
78
+
79
+ main().catch(() => process.exit(0));
@@ -0,0 +1,100 @@
1
+ # Damage control pattern library.
2
+ #
3
+ # The pre-Bash / pre-Edit / pre-Write hooks load this file and reject
4
+ # tool invocations whose payload matches any pattern below. Each entry
5
+ # explains WHY it's blocked so the error message can cite the rule.
6
+ #
7
+ # Pattern syntax:
8
+ # - `regex`: JS RegExp source (used with `new RegExp(pattern, 'i')`)
9
+ # - `kind`: one of bash | edit | write — which hook applies the rule
10
+ # - `severity`: error (block, exit 2) | warn (allow but log warning)
11
+ # - `reason`: human-readable rationale shown in the block message
12
+ #
13
+ # Adding a pattern: keep it surgical. False positives are worse than
14
+ # missed catches because they train users to bypass the guard.
15
+
16
+ patterns:
17
+ # --- Catastrophic filesystem operations ---
18
+ - regex: 'rm\s+-[a-zA-Z]*r[a-zA-Z]*f[a-zA-Z]*\s+(/|~|\$HOME)\b'
19
+ kind: bash
20
+ severity: error
21
+ reason: rm -rf on / or $HOME is almost certainly a mistake — refuse outright
22
+
23
+ - regex: 'rm\s+-[a-zA-Z]*[rfRF][a-zA-Z]*\s+/\*'
24
+ kind: bash
25
+ severity: error
26
+ reason: rm -rf /* deletes the entire filesystem
27
+
28
+ - regex: '\bdd\s+if=.*\s+of=/dev/(sd|nvme|hd)'
29
+ kind: bash
30
+ severity: error
31
+ reason: dd to a raw block device wipes the disk
32
+
33
+ - regex: '\bmkfs\.\w+\s+/dev/'
34
+ kind: bash
35
+ severity: error
36
+ reason: mkfs on a device formats it — destroys all data
37
+
38
+ - regex: '>\s*/dev/(sd|nvme|hd)\w+'
39
+ kind: bash
40
+ severity: error
41
+ reason: writing directly to a block device corrupts the disk
42
+
43
+ - regex: ':\(\)\{\s*:\|:\&\s*\};:'
44
+ kind: bash
45
+ severity: error
46
+ reason: classic fork bomb
47
+
48
+ # --- Risky git operations ---
49
+ - regex: 'git\s+push\s+.*--force(\s|$)'
50
+ kind: bash
51
+ severity: warn
52
+ reason: force push can rewrite shared history — confirm intent
53
+
54
+ - regex: 'git\s+(reset|checkout)\s+.*--hard'
55
+ kind: bash
56
+ severity: warn
57
+ reason: --hard discards uncommitted work — confirm before running
58
+
59
+ - regex: 'git\s+clean\s+-[a-zA-Z]*[fdx]+'
60
+ kind: bash
61
+ severity: warn
62
+ reason: git clean -fdx removes untracked files including .gitignored — confirm
63
+
64
+ # --- Edit / Write path scope ---
65
+ # The edit/write hooks evaluate the file_path payload, not the command.
66
+ - regex: "^/etc/(passwd|shadow|sudoers)$"
67
+ kind: edit
68
+ severity: error
69
+ reason: editing system credential files would compromise the host
70
+
71
+ - regex: "^/etc/(passwd|shadow|sudoers)$"
72
+ kind: write
73
+ severity: error
74
+ reason: writing to system credential files would compromise the host
75
+
76
+ - regex: '\.ssh/(id_[a-z]+|authorized_keys|known_hosts)$'
77
+ kind: edit
78
+ severity: warn
79
+ reason: editing SSH credentials — confirm this is intentional
80
+
81
+ - regex: '\.ssh/(id_[a-z]+|authorized_keys|known_hosts)$'
82
+ kind: write
83
+ severity: warn
84
+ reason: writing to SSH credentials — confirm this is intentional
85
+
86
+ - regex: '\.env(\.|$)'
87
+ kind: write
88
+ severity: warn
89
+ reason: writing to a .env file — confirm it does not commit secrets
90
+
91
+ # --- npm / pnpm risk ---
92
+ - regex: 'npm\s+publish(\s|$)'
93
+ kind: bash
94
+ severity: warn
95
+ reason: npm publish ships to a public registry — confirm version + auth
96
+
97
+ - regex: 'rm\s+-rf?\s+(node_modules|\.git)\b'
98
+ kind: bash
99
+ severity: warn
100
+ reason: nuking node_modules / .git is recoverable but slow — confirm
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Core hook: damage-control-write (PreToolUse, matcher: Write).
4
+ *
5
+ * Mirror of damage-control-edit.js for the Write tool. Same patterns
6
+ * file (kind: write entries). Blocks writing system credential files,
7
+ * warns on writes to .ssh/ and .env*.
8
+ */
9
+ const fs = require("fs");
10
+ const path = require("path");
11
+ const yaml = require("js-yaml");
12
+
13
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
14
+ const patternsPath = path.join(
15
+ projectDir,
16
+ ".agileflow/plugins/core/hooks/damage-control-patterns.yaml",
17
+ );
18
+
19
+ async function readStdin() {
20
+ const chunks = [];
21
+ for await (const c of process.stdin) chunks.push(c);
22
+ return Buffer.concat(chunks).toString("utf8");
23
+ }
24
+
25
+ async function main() {
26
+ const raw = await readStdin();
27
+ let payload;
28
+ try {
29
+ payload = JSON.parse(raw);
30
+ } catch {
31
+ process.exit(0);
32
+ }
33
+ const filePath =
34
+ payload &&
35
+ payload.tool_input &&
36
+ typeof payload.tool_input.file_path === "string"
37
+ ? payload.tool_input.file_path
38
+ : "";
39
+ if (!filePath) process.exit(0);
40
+
41
+ let patterns = [];
42
+ try {
43
+ const parsed = yaml.load(fs.readFileSync(patternsPath, "utf8"));
44
+ patterns = Array.isArray(parsed && parsed.patterns) ? parsed.patterns : [];
45
+ } catch (err) {
46
+ process.stderr.write(
47
+ `agileflow damage-control: WARNING — patterns file unreadable (${err.code || err.name}: ${patternsPath}). Write safety guards are DISABLED until this is fixed.\n`,
48
+ );
49
+ process.exit(0);
50
+ }
51
+
52
+ for (const p of patterns) {
53
+ if (p.kind !== "write") continue;
54
+ let re;
55
+ try {
56
+ re = new RegExp(p.regex, "i");
57
+ } catch {
58
+ continue;
59
+ }
60
+ if (re.test(filePath)) {
61
+ if (p.severity === "error") {
62
+ process.stderr.write(
63
+ `agileflow damage-control: BLOCKED write — ${p.reason}\n path: ${filePath}\n`,
64
+ );
65
+ process.exit(2);
66
+ }
67
+ process.stderr.write(
68
+ `agileflow damage-control: WARN — ${p.reason} (path: ${filePath})\n`,
69
+ );
70
+ }
71
+ }
72
+ process.exit(0);
73
+ }
74
+
75
+ main().catch(() => process.exit(0));