@ikon85/agent-workflow-kit 0.25.0 → 0.26.1

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.
@@ -7,10 +7,10 @@ description: "Preview and transactionally apply a parity-verified agent-workflow
7
7
 
8
8
  Update an installed consumer only from the public scoped package
9
9
  `@ikon85/agent-workflow-kit`. The unscoped `agent-workflow-kit` package belongs
10
- to another publisher and is never a valid update source. Existing installs from
11
- `npx github:iKon85/agent-workflow-kit` remain valid, but updates must use the
12
- scoped npm release so the shared release-parity checker can prove that npm and
13
- the matching GitHub release contain the same artifact.
10
+ to another publisher and is never a valid update source. Existing direct
11
+ GitHub installs remain valid, but updates must use the scoped npm release so
12
+ the shared release-parity checker can prove that npm and the matching GitHub
13
+ release contain the same artifact.
14
14
 
15
15
  ## Workflow
16
16
 
@@ -26,7 +26,7 @@ This is a prompt-driven skill, not a deterministic script. Explore, present what
26
26
  | `## Agent skills` + `## Prod` in CLAUDE.md **and** AGENTS.md | one-line pointers + deploy target (Sections C/G + Write) |
27
27
  | `.github/workflows/agent-workflow-kit-update.yml` | optional tested Kit update pull request for GitHub consumers (Section A2) |
28
28
  | `docs/agents/census.md` | optional-census choice, paths, state, and safe disable contract seeded from [census.md](./census.md) (Section A3) |
29
- | `docs/agents/workflow-capabilities.json` + capability assets | consumer-owned Worktree Lifecycle, Memory Lifecycle, and Workflow Advisories choices (Sections A4–A6) |
29
+ | `docs/agents/workflow-capabilities.json` + capability assets | consumer-owned Worktree Lifecycle, Memory Lifecycle, Workflow Advisories, and Safety Guardrails choices (Sections A4–A7) |
30
30
 
31
31
  ## Idempotency contract — read before writing anything
32
32
 
@@ -57,7 +57,7 @@ Read the current state; don't assume. For every target file, read its first line
57
57
  - `CONTEXT.md`, `CONTEXT-MAP.md`, `docs/adr/` — domain-doc layout.
58
58
  - `docs/agents/`, `docs/agents/skills/`, `docs/conventions/` — prior output of this skill.
59
59
  - `docs/agents/census.md`, `.census/profile.json`, `.census/active.json` — an existing census choice or consumer-owned census to adopt.
60
- - `docs/agents/workflow-capabilities.json`, `.claude/settings.json` — existing capability choices/profile plus Worktree Lifecycle and Workflow Advisories hook wiring to adopt.
60
+ - `docs/agents/workflow-capabilities.json`, `.claude/settings.json`, `git config --get core.hooksPath` — existing capability choices/profile plus Worktree Lifecycle, Workflow Advisories, and Safety Guardrails wiring to adopt.
61
61
  - `.memory/active/`, `.memory/archive/`, and `.memory/receipts/` — consumer-owned Memory Lifecycle policies or recovery evidence to adopt without normalization.
62
62
  - `gh auth status` (if GitHub) — is `gh` authenticated, and with which scopes?
63
63
 
@@ -218,6 +218,31 @@ repository?"* Offer exactly **Yes**, **Later**, and **No**.
218
218
  - **Disable** — remove exact kit-owned commands first, then set `enabled:
219
219
  false`; preserve every profile value and unknown key.
220
220
 
221
+ ### 2f. Section A7 — Optional Safety Guardrails
222
+
223
+ > Safety Guardrails is a counted group of seven independently selectable
224
+ > protections: four agent-tool guards and three repository-security
225
+ > capabilities. Setup stages and validates the chosen profile plus wiring
226
+ > before activation; disabled rows remain exact no-ops.
227
+
228
+ Read [safety-guardrails.md](./safety-guardrails.md) in full. Reconcile its
229
+ complete `yes / later / no / existing / disable` matrix. If no choice exists,
230
+ ask: *"Should setup configure the portable Safety Guardrails for this
231
+ repository?"* Offer exactly **Yes**, **Later**, and **No**.
232
+
233
+ - **Yes** — ask which of the seven counted rows to enable, then reconcile only
234
+ `safetyGuardrails`, exact selected Agent-hook commands, and the selected
235
+ native Git-hook path. Report `N of 7 enabled`.
236
+ - **Later / No** — record the choice without Agent- or Git-hook wiring.
237
+ - **Existing** — adopt profile and wiring byte-safely; preserve unknown keys
238
+ and consumer-specific policies.
239
+ - **Disable** — remove only exact kit-owned Agent commands and Git wiring
240
+ first, then set `enabled: false`.
241
+
242
+ The seed's reference consumer uses the parity-on profile. Other consumers are
243
+ opt-in/manual: never copy its package-manager, path, shim, surface, or outage
244
+ decisions without repository evidence and user confirmation.
245
+
221
246
  ### 3. Section B — Triage labels
222
247
 
223
248
  > When `triage` processes an incoming issue it applies labels (or your tracker's equivalent). It needs strings you've actually configured, or it creates duplicates.
@@ -305,10 +330,11 @@ request changes a recorded choice.
305
330
 
306
331
  For `docs/agents/workflow-capabilities.json`, reconcile only each selected
307
332
  capability section. Never replace the whole file: Project Release, Memory
308
- Lifecycle, Worktree Lifecycle, Workflow Advisories, future sections, and
333
+ Lifecycle, Worktree Lifecycle, Workflow Advisories, Safety Guardrails, future sections, and
309
334
  unknown consumer keys share this profile. Apply the transition and hook
310
- ownership rules from [worktree-lifecycle.md](./worktree-lifecycle.md) and
311
- [workflow-advisories.md](./workflow-advisories.md) transactionally.
335
+ ownership rules from [worktree-lifecycle.md](./worktree-lifecycle.md),
336
+ [workflow-advisories.md](./workflow-advisories.md), and
337
+ [safety-guardrails.md](./safety-guardrails.md) transactionally.
312
338
 
313
339
  For Memory Lifecycle, use only the deterministic setup helper from Section A5.
314
340
  Do not copy templates with shell commands or edit the capability profile by
@@ -0,0 +1,155 @@
1
+ # Safety Guardrails setup contract
2
+
3
+ Safety Guardrails is a counted, opt-in capability group backed by the
4
+ consumer-owned `docs/agents/workflow-capabilities.json` profile. Its seven
5
+ rows are selected independently: four protect agent tool calls and three
6
+ bootstrap repository security. No repository name, package manager, hook
7
+ surface, threshold, path, or outage policy is inferred from the kit.
8
+
9
+ ## Choice matrix
10
+
11
+ | State | Setup action |
12
+ |---|---|
13
+ | `missing` | Ask `yes / later / no`; do not infer or write before the answer. |
14
+ | `yes` | Ask which rows to enable, stage profile plus selected wiring, validate, then activate. |
15
+ | `later` | Record the retryable deferral; do not activate any wiring. |
16
+ | `no` | Record the opt-out; do not activate any wiring. |
17
+ | `existing` | Adopt the consumer profile and existing wiring byte-safely. |
18
+ | `disable` | Remove exact kit-owned Agent and Git wiring first, then set `enabled: false`. |
19
+
20
+ ```json safety-guardrails-setup-effects
21
+ [
22
+ {"state":"missing","choice":null,"operations":[]},
23
+ {"state":"yes","choice":"yes","operations":["record-choice","stage-enabled-profile","stage-agent-hook-wiring","stage-git-hook-wiring","activate-staged-profile"]},
24
+ {"state":"later","choice":"later","operations":["record-choice"]},
25
+ {"state":"no","choice":"no","operations":["record-choice"]},
26
+ {"state":"existing","choice":"yes","operations":["adopt-existing"]},
27
+ {"state":"disable","choice":"yes","operations":["remove-agent-hook-wiring","remove-git-hook-wiring","update-profile-disabled"]}
28
+ ]
29
+ ```
30
+
31
+ ## Counted capability map
32
+
33
+ Every historical Safety row maps to one profile decision and one shipped
34
+ artifact. Setup reports the result as `N of 7 enabled`, never as one
35
+ all-or-nothing switch.
36
+
37
+ ```json safety-guardrails-capabilities
38
+ [
39
+ {"id":"secrets","profilePath":"safetyGuardrails.secrets.enabled","artifact":".claude/hooks/block-secrets.py"},
40
+ {"id":"packageManager","profilePath":"safetyGuardrails.packageManager.enabled","artifact":".claude/hooks/block-npm-install-in-pnpm.py"},
41
+ {"id":"doubleBackground","profilePath":"safetyGuardrails.doubleBackground.enabled","artifact":".claude/hooks/block-bg-double-background.py"},
42
+ {"id":"searchShim","profilePath":"safetyGuardrails.searchShim.enabled","artifact":".claude/hooks/grep-shim-guard.py"},
43
+ {"id":"gitHooks","profilePath":"safetyGuardrails.repositorySecurity.gitHooks.enabled","artifact":"scripts/security/install-git-hooks.mjs"},
44
+ {"id":"gitleaks","profilePath":"safetyGuardrails.repositorySecurity.gitleaks.enabled","artifact":"scripts/security/ensure-gitleaks.mjs"},
45
+ {"id":"dependencyAudit","profilePath":"safetyGuardrails.repositorySecurity.dependencyAudit.enabled","artifact":"scripts/security/audit-gate.mjs"}
46
+ ]
47
+ ```
48
+
49
+ ## Profile shape
50
+
51
+ Reconcile only `safetyGuardrails`; preserve every sibling section, unknown key,
52
+ and consumer-owned value. The following is the reference consumer's parity-on
53
+ example, not a universal default. Other consumers choose each `enabled` value
54
+ and fill only facts proven in their repository.
55
+
56
+ ```json
57
+ {
58
+ "safetyGuardrails": {
59
+ "choice": "yes",
60
+ "enabled": true,
61
+ "secrets": {
62
+ "enabled": true,
63
+ "sensitiveNames": [".env", ".npmrc", "credentials.json"],
64
+ "sensitivePathFragments": ["secrets/", ".ssh/"],
65
+ "safeTemplateSuffixes": [".example", ".sample", ".template", ".dist"]
66
+ },
67
+ "packageManager": {
68
+ "enabled": true,
69
+ "lockfiles": {
70
+ "pnpm-lock.yaml": "pnpm",
71
+ "package-lock.json": "npm",
72
+ "yarn.lock": "yarn",
73
+ "bun.lockb": "bun"
74
+ }
75
+ },
76
+ "doubleBackground": {"enabled": true, "surfaces": ["claude"]},
77
+ "searchShim": {
78
+ "enabled": true,
79
+ "detected": true,
80
+ "commandNames": ["grep", "rg"]
81
+ },
82
+ "repositorySecurity": {
83
+ "gitHooks": {"enabled": true, "hooksPath": ".githooks"},
84
+ "gitleaks": {"enabled": true, "required": false},
85
+ "dependencyAudit": {
86
+ "enabled": true,
87
+ "packageManager": "pnpm",
88
+ "outagePolicy": "warn",
89
+ "blockSeverities": ["high", "critical"]
90
+ }
91
+ }
92
+ }
93
+ }
94
+ ```
95
+
96
+ An empty or disabled row is an honest no-op. `searchShim.detected` is a
97
+ consumer fact, not an installation request. `gitleaks.required: false` keeps
98
+ the verified optional provisioner non-blocking when the platform or network is
99
+ unsupported. Dependency-audit outage policy must be explicit.
100
+
101
+ ## Agent-hook ownership
102
+
103
+ Wire only enabled rows, using the exact kit-owned commands:
104
+
105
+ - `secrets` — `python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/block-secrets.py"`
106
+ - `packageManager` —
107
+ `python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/block-npm-install-in-pnpm.py"`
108
+ - `doubleBackground` —
109
+ `python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/block-bg-double-background.py"`
110
+ - `searchShim` —
111
+ `python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/grep-shim-guard.py"`
112
+
113
+ All four are PreToolUse adapters. Preserve unrelated matchers and commands in
114
+ `.claude/settings.json`. Disable removes only these exact commands.
115
+
116
+ ## Repository wiring
117
+
118
+ When `gitHooks.enabled` is true, run:
119
+
120
+ ```bash
121
+ node scripts/security/install-git-hooks.mjs <hooksPath>
122
+ ```
123
+
124
+ only after confirming the configured directory exists and contains the
125
+ consumer's intended hooks. Never create or replace consumer hook bodies.
126
+
127
+ When `gitleaks.enabled` is true, the pinned profile may be provisioned through
128
+ `scripts/security/ensure-gitleaks.mjs`; checksum mismatch, unsupported platform,
129
+ offline state, and unwritable destination remain distinct visible outcomes.
130
+ Never read a secret file to test the provisioner.
131
+
132
+ When `dependencyAudit.enabled` is true, use
133
+ `scripts/security/audit-gate.mjs` with the configured package manager and
134
+ outage policy. High/Critical findings block; Low/Moderate findings pass.
135
+
136
+ ## Transaction and failure contract
137
+
138
+ Prepare the next profile and `.claude/settings.json` in same-directory staging
139
+ files. Validate JSON, all selected artifact paths, the seven-row count, the
140
+ configured Git-hook directory, and package-manager facts before mutation.
141
+ Then:
142
+
143
+ 1. reconcile selected Agent commands in the staged settings;
144
+ 2. remember the prior `core.hooksPath` and apply the selected Git wiring;
145
+ 3. atomically rename staged settings and profile into place;
146
+ 4. on any failure, restore the prior Git config and leave active files
147
+ byte-for-byte unchanged.
148
+
149
+ Disable reverses the safety order: remove exact Agent commands and restore or
150
+ unset only the kit-owned Git wiring before committing `enabled: false`.
151
+ Repeated activation with the same choices is byte-idempotent.
152
+
153
+ Setup and verification may inspect paths, profile metadata, command exit
154
+ status, advisory counts, and hashes. They must never read secret contents or
155
+ print environment/config values.
@@ -7,10 +7,10 @@ description: "Preview and transactionally apply a parity-verified agent-workflow
7
7
 
8
8
  Update an installed consumer only from the public scoped package
9
9
  `@ikon85/agent-workflow-kit`. The unscoped `agent-workflow-kit` package belongs
10
- to another publisher and is never a valid update source. Existing installs from
11
- `npx github:iKon85/agent-workflow-kit` remain valid, but updates must use the
12
- scoped npm release so the shared release-parity checker can prove that npm and
13
- the matching GitHub release contain the same artifact.
10
+ to another publisher and is never a valid update source. Existing direct
11
+ GitHub installs remain valid, but updates must use the scoped npm release so
12
+ the shared release-parity checker can prove that npm and the matching GitHub
13
+ release contain the same artifact.
14
14
 
15
15
  ## Workflow
16
16
 
@@ -26,7 +26,7 @@ This is a prompt-driven skill, not a deterministic script. Explore, present what
26
26
  | `## Agent skills` + `## Prod` in CLAUDE.md **and** AGENTS.md | one-line pointers + deploy target (Sections C/G + Write) |
27
27
  | `.github/workflows/agent-workflow-kit-update.yml` | optional tested Kit update pull request for GitHub consumers (Section A2) |
28
28
  | `docs/agents/census.md` | optional-census choice, paths, state, and safe disable contract seeded from [census.md](./census.md) (Section A3) |
29
- | `docs/agents/workflow-capabilities.json` + capability assets | consumer-owned Worktree Lifecycle, Memory Lifecycle, and Workflow Advisories choices (Sections A4–A6) |
29
+ | `docs/agents/workflow-capabilities.json` + capability assets | consumer-owned Worktree Lifecycle, Memory Lifecycle, Workflow Advisories, and Safety Guardrails choices (Sections A4–A7) |
30
30
 
31
31
  ## Idempotency contract — read before writing anything
32
32
 
@@ -57,7 +57,7 @@ Read the current state; don't assume. For every target file, read its first line
57
57
  - `CONTEXT.md`, `CONTEXT-MAP.md`, `docs/adr/` — domain-doc layout.
58
58
  - `docs/agents/`, `docs/agents/skills/`, `docs/conventions/` — prior output of this skill.
59
59
  - `docs/agents/census.md`, `.census/profile.json`, `.census/active.json` — an existing census choice or consumer-owned census to adopt.
60
- - `docs/agents/workflow-capabilities.json`, `.claude/settings.json` — existing capability choices/profile plus Worktree Lifecycle and Workflow Advisories hook wiring to adopt.
60
+ - `docs/agents/workflow-capabilities.json`, `.claude/settings.json`, `git config --get core.hooksPath` — existing capability choices/profile plus Worktree Lifecycle, Workflow Advisories, and Safety Guardrails wiring to adopt.
61
61
  - `.memory/active/`, `.memory/archive/`, and `.memory/receipts/` — consumer-owned Memory Lifecycle policies or recovery evidence to adopt without normalization.
62
62
  - `gh auth status` (if GitHub) — is `gh` authenticated, and with which scopes?
63
63
 
@@ -218,6 +218,31 @@ repository?"* Offer exactly **Yes**, **Later**, and **No**.
218
218
  - **Disable** — remove exact kit-owned commands first, then set `enabled:
219
219
  false`; preserve every profile value and unknown key.
220
220
 
221
+ ### 2f. Section A7 — Optional Safety Guardrails
222
+
223
+ > Safety Guardrails is a counted group of seven independently selectable
224
+ > protections: four agent-tool guards and three repository-security
225
+ > capabilities. Setup stages and validates the chosen profile plus wiring
226
+ > before activation; disabled rows remain exact no-ops.
227
+
228
+ Read [safety-guardrails.md](./safety-guardrails.md) in full. Reconcile its
229
+ complete `yes / later / no / existing / disable` matrix. If no choice exists,
230
+ ask: *"Should setup configure the portable Safety Guardrails for this
231
+ repository?"* Offer exactly **Yes**, **Later**, and **No**.
232
+
233
+ - **Yes** — ask which of the seven counted rows to enable, then reconcile only
234
+ `safetyGuardrails`, exact selected Agent-hook commands, and the selected
235
+ native Git-hook path. Report `N of 7 enabled`.
236
+ - **Later / No** — record the choice without Agent- or Git-hook wiring.
237
+ - **Existing** — adopt profile and wiring byte-safely; preserve unknown keys
238
+ and consumer-specific policies.
239
+ - **Disable** — remove only exact kit-owned Agent commands and Git wiring
240
+ first, then set `enabled: false`.
241
+
242
+ The seed's reference consumer uses the parity-on profile. Other consumers are
243
+ opt-in/manual: never copy its package-manager, path, shim, surface, or outage
244
+ decisions without repository evidence and user confirmation.
245
+
221
246
  ### 3. Section B — Triage labels
222
247
 
223
248
  > When `triage` processes an incoming issue it applies labels (or your tracker's equivalent). It needs strings you've actually configured, or it creates duplicates.
@@ -305,10 +330,11 @@ request changes a recorded choice.
305
330
 
306
331
  For `docs/agents/workflow-capabilities.json`, reconcile only each selected
307
332
  capability section. Never replace the whole file: Project Release, Memory
308
- Lifecycle, Worktree Lifecycle, Workflow Advisories, future sections, and
333
+ Lifecycle, Worktree Lifecycle, Workflow Advisories, Safety Guardrails, future sections, and
309
334
  unknown consumer keys share this profile. Apply the transition and hook
310
- ownership rules from [worktree-lifecycle.md](./worktree-lifecycle.md) and
311
- [workflow-advisories.md](./workflow-advisories.md) transactionally.
335
+ ownership rules from [worktree-lifecycle.md](./worktree-lifecycle.md),
336
+ [workflow-advisories.md](./workflow-advisories.md), and
337
+ [safety-guardrails.md](./safety-guardrails.md) transactionally.
312
338
 
313
339
  For Memory Lifecycle, use only the deterministic setup helper from Section A5.
314
340
  Do not copy templates with shell commands or edit the capability profile by
@@ -0,0 +1,155 @@
1
+ # Safety Guardrails setup contract
2
+
3
+ Safety Guardrails is a counted, opt-in capability group backed by the
4
+ consumer-owned `docs/agents/workflow-capabilities.json` profile. Its seven
5
+ rows are selected independently: four protect agent tool calls and three
6
+ bootstrap repository security. No repository name, package manager, hook
7
+ surface, threshold, path, or outage policy is inferred from the kit.
8
+
9
+ ## Choice matrix
10
+
11
+ | State | Setup action |
12
+ |---|---|
13
+ | `missing` | Ask `yes / later / no`; do not infer or write before the answer. |
14
+ | `yes` | Ask which rows to enable, stage profile plus selected wiring, validate, then activate. |
15
+ | `later` | Record the retryable deferral; do not activate any wiring. |
16
+ | `no` | Record the opt-out; do not activate any wiring. |
17
+ | `existing` | Adopt the consumer profile and existing wiring byte-safely. |
18
+ | `disable` | Remove exact kit-owned Agent and Git wiring first, then set `enabled: false`. |
19
+
20
+ ```json safety-guardrails-setup-effects
21
+ [
22
+ {"state":"missing","choice":null,"operations":[]},
23
+ {"state":"yes","choice":"yes","operations":["record-choice","stage-enabled-profile","stage-agent-hook-wiring","stage-git-hook-wiring","activate-staged-profile"]},
24
+ {"state":"later","choice":"later","operations":["record-choice"]},
25
+ {"state":"no","choice":"no","operations":["record-choice"]},
26
+ {"state":"existing","choice":"yes","operations":["adopt-existing"]},
27
+ {"state":"disable","choice":"yes","operations":["remove-agent-hook-wiring","remove-git-hook-wiring","update-profile-disabled"]}
28
+ ]
29
+ ```
30
+
31
+ ## Counted capability map
32
+
33
+ Every historical Safety row maps to one profile decision and one shipped
34
+ artifact. Setup reports the result as `N of 7 enabled`, never as one
35
+ all-or-nothing switch.
36
+
37
+ ```json safety-guardrails-capabilities
38
+ [
39
+ {"id":"secrets","profilePath":"safetyGuardrails.secrets.enabled","artifact":".claude/hooks/block-secrets.py"},
40
+ {"id":"packageManager","profilePath":"safetyGuardrails.packageManager.enabled","artifact":".claude/hooks/block-npm-install-in-pnpm.py"},
41
+ {"id":"doubleBackground","profilePath":"safetyGuardrails.doubleBackground.enabled","artifact":".claude/hooks/block-bg-double-background.py"},
42
+ {"id":"searchShim","profilePath":"safetyGuardrails.searchShim.enabled","artifact":".claude/hooks/grep-shim-guard.py"},
43
+ {"id":"gitHooks","profilePath":"safetyGuardrails.repositorySecurity.gitHooks.enabled","artifact":"scripts/security/install-git-hooks.mjs"},
44
+ {"id":"gitleaks","profilePath":"safetyGuardrails.repositorySecurity.gitleaks.enabled","artifact":"scripts/security/ensure-gitleaks.mjs"},
45
+ {"id":"dependencyAudit","profilePath":"safetyGuardrails.repositorySecurity.dependencyAudit.enabled","artifact":"scripts/security/audit-gate.mjs"}
46
+ ]
47
+ ```
48
+
49
+ ## Profile shape
50
+
51
+ Reconcile only `safetyGuardrails`; preserve every sibling section, unknown key,
52
+ and consumer-owned value. The following is the reference consumer's parity-on
53
+ example, not a universal default. Other consumers choose each `enabled` value
54
+ and fill only facts proven in their repository.
55
+
56
+ ```json
57
+ {
58
+ "safetyGuardrails": {
59
+ "choice": "yes",
60
+ "enabled": true,
61
+ "secrets": {
62
+ "enabled": true,
63
+ "sensitiveNames": [".env", ".npmrc", "credentials.json"],
64
+ "sensitivePathFragments": ["secrets/", ".ssh/"],
65
+ "safeTemplateSuffixes": [".example", ".sample", ".template", ".dist"]
66
+ },
67
+ "packageManager": {
68
+ "enabled": true,
69
+ "lockfiles": {
70
+ "pnpm-lock.yaml": "pnpm",
71
+ "package-lock.json": "npm",
72
+ "yarn.lock": "yarn",
73
+ "bun.lockb": "bun"
74
+ }
75
+ },
76
+ "doubleBackground": {"enabled": true, "surfaces": ["claude"]},
77
+ "searchShim": {
78
+ "enabled": true,
79
+ "detected": true,
80
+ "commandNames": ["grep", "rg"]
81
+ },
82
+ "repositorySecurity": {
83
+ "gitHooks": {"enabled": true, "hooksPath": ".githooks"},
84
+ "gitleaks": {"enabled": true, "required": false},
85
+ "dependencyAudit": {
86
+ "enabled": true,
87
+ "packageManager": "pnpm",
88
+ "outagePolicy": "warn",
89
+ "blockSeverities": ["high", "critical"]
90
+ }
91
+ }
92
+ }
93
+ }
94
+ ```
95
+
96
+ An empty or disabled row is an honest no-op. `searchShim.detected` is a
97
+ consumer fact, not an installation request. `gitleaks.required: false` keeps
98
+ the verified optional provisioner non-blocking when the platform or network is
99
+ unsupported. Dependency-audit outage policy must be explicit.
100
+
101
+ ## Agent-hook ownership
102
+
103
+ Wire only enabled rows, using the exact kit-owned commands:
104
+
105
+ - `secrets` — `python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/block-secrets.py"`
106
+ - `packageManager` —
107
+ `python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/block-npm-install-in-pnpm.py"`
108
+ - `doubleBackground` —
109
+ `python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/block-bg-double-background.py"`
110
+ - `searchShim` —
111
+ `python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/grep-shim-guard.py"`
112
+
113
+ All four are PreToolUse adapters. Preserve unrelated matchers and commands in
114
+ `.claude/settings.json`. Disable removes only these exact commands.
115
+
116
+ ## Repository wiring
117
+
118
+ When `gitHooks.enabled` is true, run:
119
+
120
+ ```bash
121
+ node scripts/security/install-git-hooks.mjs <hooksPath>
122
+ ```
123
+
124
+ only after confirming the configured directory exists and contains the
125
+ consumer's intended hooks. Never create or replace consumer hook bodies.
126
+
127
+ When `gitleaks.enabled` is true, the pinned profile may be provisioned through
128
+ `scripts/security/ensure-gitleaks.mjs`; checksum mismatch, unsupported platform,
129
+ offline state, and unwritable destination remain distinct visible outcomes.
130
+ Never read a secret file to test the provisioner.
131
+
132
+ When `dependencyAudit.enabled` is true, use
133
+ `scripts/security/audit-gate.mjs` with the configured package manager and
134
+ outage policy. High/Critical findings block; Low/Moderate findings pass.
135
+
136
+ ## Transaction and failure contract
137
+
138
+ Prepare the next profile and `.claude/settings.json` in same-directory staging
139
+ files. Validate JSON, all selected artifact paths, the seven-row count, the
140
+ configured Git-hook directory, and package-manager facts before mutation.
141
+ Then:
142
+
143
+ 1. reconcile selected Agent commands in the staged settings;
144
+ 2. remember the prior `core.hooksPath` and apply the selected Git wiring;
145
+ 3. atomically rename staged settings and profile into place;
146
+ 4. on any failure, restore the prior Git config and leave active files
147
+ byte-for-byte unchanged.
148
+
149
+ Disable reverses the safety order: remove exact Agent commands and restore or
150
+ unset only the kit-owned Git wiring before committing `enabled: false`.
151
+ Repeated activation with the same choices is byte-idempotent.
152
+
153
+ Setup and verification may inspect paths, profile metadata, command exit
154
+ status, advisory counts, and hashes. They must never read secret contents or
155
+ print environment/config values.
package/README.md CHANGED
@@ -332,6 +332,34 @@ still reference. Flags: `--force` (overwrite pre-existing files on `init`),
332
332
 
333
333
  ## Release notes
334
334
 
335
+ ### 0.26.1
336
+
337
+ - changed: `.agents/skills/kit-update/SKILL.md`
338
+ - changed: `.agents/skills/setup-workflow/SKILL.md`
339
+ - changed: `.agents/skills/setup-workflow/safety-guardrails.md`
340
+ - changed: `.claude/skills/kit-update/SKILL.md`
341
+ - changed: `.claude/skills/setup-workflow/SKILL.md`
342
+ - changed: `.claude/skills/setup-workflow/safety-guardrails.md`
343
+ - changed: `scripts/release-delta-guard.mjs`
344
+
345
+ ### 0.26.0
346
+
347
+ - added: `.agents/skills/setup-workflow/safety-guardrails.md`
348
+ - added: `.claude/hooks/_safety_guard.py`
349
+ - added: `.claude/hooks/block-bg-double-background.py`
350
+ - added: `.claude/hooks/block-npm-install-in-pnpm.py`
351
+ - added: `.claude/hooks/block-secrets.py`
352
+ - added: `.claude/hooks/grep-shim-guard.py`
353
+ - added: `.claude/skills/setup-workflow/safety-guardrails.md`
354
+ - added: `scripts/safety-guardrails/core.py`
355
+ - added: `scripts/safety-guardrails/search.py`
356
+ - added: `scripts/security/audit-gate.mjs`
357
+ - added: `scripts/security/ensure-gitleaks.mjs`
358
+ - added: `scripts/security/gitleaks-profile.json`
359
+ - added: `scripts/security/install-git-hooks.mjs`
360
+ - changed: `.agents/skills/setup-workflow/SKILL.md`
361
+ - changed: `.claude/skills/setup-workflow/SKILL.md`
362
+
335
363
  ### 0.25.0
336
364
 
337
365
  - added: `.claude/hooks/convention-drift-hint.py`
@@ -1,5 +1,5 @@
1
1
  {
2
- "kitVersion": "0.25.0",
2
+ "kitVersion": "0.26.1",
3
3
  "files": [
4
4
  {
5
5
  "path": ".agents/skills/ask-matt/SKILL.md",
@@ -339,7 +339,7 @@
339
339
  "kind": "skill",
340
340
  "ownerSkill": "kit-update",
341
341
  "surface": "codex",
342
- "sha256": "5b7a8c11b5852ee115fa8d3f66a3938d7b4db173a47d711da48abab635b2c261",
342
+ "sha256": "dbf2c0f696bd0ce45c5c2a315d40a3bbb99635e4ab384c788ac59f799fbf43de",
343
343
  "mode": 420,
344
344
  "origin": "kit"
345
345
  },
@@ -568,12 +568,21 @@
568
568
  "mode": 420,
569
569
  "origin": "kit"
570
570
  },
571
+ {
572
+ "path": ".agents/skills/setup-workflow/safety-guardrails.md",
573
+ "kind": "skill",
574
+ "ownerSkill": "setup-workflow",
575
+ "surface": "codex",
576
+ "sha256": "8e223f625dcf941142005cbd9a4b6214358f68e3fd63cbee7bffef35b8b14ec8",
577
+ "mode": 420,
578
+ "origin": "kit"
579
+ },
571
580
  {
572
581
  "path": ".agents/skills/setup-workflow/SKILL.md",
573
582
  "kind": "skill",
574
583
  "ownerSkill": "setup-workflow",
575
584
  "surface": "codex",
576
- "sha256": "96c0c3ba9ed6b194030a8c45be618caf934e9c4ca8415ea19d726af33cd763cb",
585
+ "sha256": "e4cac2f46ac5f16e9a1473b38a64979a7d767ccdc834a2540d20c733305ea2e7",
577
586
  "mode": 420,
578
587
  "origin": "kit"
579
588
  },
@@ -890,6 +899,13 @@
890
899
  "mode": 420,
891
900
  "origin": "kit"
892
901
  },
902
+ {
903
+ "path": ".claude/hooks/_safety_guard.py",
904
+ "kind": "hook",
905
+ "sha256": "1f5c387b11731c7b1cd027fc58acd4c1e45c64799975dc50eeff72469df0deb0",
906
+ "mode": 420,
907
+ "origin": "kit"
908
+ },
893
909
  {
894
910
  "path": ".claude/hooks/baseline-capture-hint.py",
895
911
  "kind": "hook",
@@ -897,6 +913,27 @@
897
913
  "mode": 493,
898
914
  "origin": "kit"
899
915
  },
916
+ {
917
+ "path": ".claude/hooks/block-bg-double-background.py",
918
+ "kind": "hook",
919
+ "sha256": "ad0c763e3afbb25685f91eea2c39fc3ee58c765536a961810a49bd0c7bbd47b8",
920
+ "mode": 493,
921
+ "origin": "kit"
922
+ },
923
+ {
924
+ "path": ".claude/hooks/block-npm-install-in-pnpm.py",
925
+ "kind": "hook",
926
+ "sha256": "54da075f8e63e5e927cb752c26c47880da49eb1f730f078558f3f5eec9377c82",
927
+ "mode": 493,
928
+ "origin": "kit"
929
+ },
930
+ {
931
+ "path": ".claude/hooks/block-secrets.py",
932
+ "kind": "hook",
933
+ "sha256": "962c4f7f035c5cb4c322ee7c76b707a148526000427f4ebd49bfcac50af003b4",
934
+ "mode": 493,
935
+ "origin": "kit"
936
+ },
900
937
  {
901
938
  "path": ".claude/hooks/branch-context.py",
902
939
  "kind": "hook",
@@ -946,6 +983,13 @@
946
983
  "mode": 493,
947
984
  "origin": "kit"
948
985
  },
986
+ {
987
+ "path": ".claude/hooks/grep-shim-guard.py",
988
+ "kind": "hook",
989
+ "sha256": "9f69541c60c795bd5ec2a4f5d155f6fd3ced4f2ac6a28ed0c55c9a57772dd0d2",
990
+ "mode": 493,
991
+ "origin": "kit"
992
+ },
949
993
  {
950
994
  "path": ".claude/hooks/loc-offender-forewarn.py",
951
995
  "kind": "hook",
@@ -1455,7 +1499,7 @@
1455
1499
  "kind": "skill",
1456
1500
  "ownerSkill": "kit-update",
1457
1501
  "surface": "claude",
1458
- "sha256": "5b7a8c11b5852ee115fa8d3f66a3938d7b4db173a47d711da48abab635b2c261",
1502
+ "sha256": "dbf2c0f696bd0ce45c5c2a315d40a3bbb99635e4ab384c788ac59f799fbf43de",
1459
1503
  "mode": 420,
1460
1504
  "origin": "kit"
1461
1505
  },
@@ -1702,12 +1746,21 @@
1702
1746
  "mode": 420,
1703
1747
  "origin": "kit"
1704
1748
  },
1749
+ {
1750
+ "path": ".claude/skills/setup-workflow/safety-guardrails.md",
1751
+ "kind": "skill",
1752
+ "ownerSkill": "setup-workflow",
1753
+ "surface": "claude",
1754
+ "sha256": "8e223f625dcf941142005cbd9a4b6214358f68e3fd63cbee7bffef35b8b14ec8",
1755
+ "mode": 420,
1756
+ "origin": "kit"
1757
+ },
1705
1758
  {
1706
1759
  "path": ".claude/skills/setup-workflow/SKILL.md",
1707
1760
  "kind": "skill",
1708
1761
  "ownerSkill": "setup-workflow",
1709
1762
  "surface": "claude",
1710
- "sha256": "96c0c3ba9ed6b194030a8c45be618caf934e9c4ca8415ea19d726af33cd763cb",
1763
+ "sha256": "e4cac2f46ac5f16e9a1473b38a64979a7d767ccdc834a2540d20c733305ea2e7",
1711
1764
  "mode": 420,
1712
1765
  "origin": "kit"
1713
1766
  },
@@ -2250,7 +2303,7 @@
2250
2303
  {
2251
2304
  "path": "scripts/release-delta-guard.mjs",
2252
2305
  "kind": "script",
2253
- "sha256": "37a6f27b350bb98a19a8c53e1f6ee187b3a39c0ff5317141717bf9ab65129bad",
2306
+ "sha256": "a51f30e7261e4efb0b5c5002f39353ef8e8be18a2bd39ad7139f1ce7a2e7e18d",
2254
2307
  "mode": 420,
2255
2308
  "origin": "kit"
2256
2309
  },
@@ -2268,6 +2321,48 @@
2268
2321
  "mode": 420,
2269
2322
  "origin": "kit"
2270
2323
  },
2324
+ {
2325
+ "path": "scripts/safety-guardrails/core.py",
2326
+ "kind": "script",
2327
+ "sha256": "c69c9d4e1b17efca51de12454b119e9ee6758aa6c8d73fe64e4bafd76c804f4e",
2328
+ "mode": 420,
2329
+ "origin": "kit"
2330
+ },
2331
+ {
2332
+ "path": "scripts/safety-guardrails/search.py",
2333
+ "kind": "script",
2334
+ "sha256": "345b1456f1a00320b6ad3e4fad1b050d8dd21a16b51b5e5cfdaf6a579530eac1",
2335
+ "mode": 420,
2336
+ "origin": "kit"
2337
+ },
2338
+ {
2339
+ "path": "scripts/security/audit-gate.mjs",
2340
+ "kind": "script",
2341
+ "sha256": "c6129e77f58e80f5880ebd6d2d235e27e9e31a20ee3cd967ce64815738e61e54",
2342
+ "mode": 493,
2343
+ "origin": "kit"
2344
+ },
2345
+ {
2346
+ "path": "scripts/security/ensure-gitleaks.mjs",
2347
+ "kind": "script",
2348
+ "sha256": "5591f31d88362aa0df75f7b6bb7f22c094822b4bd00c5f6bf9ccff6a2313ed56",
2349
+ "mode": 493,
2350
+ "origin": "kit"
2351
+ },
2352
+ {
2353
+ "path": "scripts/security/gitleaks-profile.json",
2354
+ "kind": "doc",
2355
+ "sha256": "2480f7b4656d28cea1ede895970275d7b97f58d396341ee5e36b3df12575c1b6",
2356
+ "mode": 420,
2357
+ "origin": "kit"
2358
+ },
2359
+ {
2360
+ "path": "scripts/security/install-git-hooks.mjs",
2361
+ "kind": "script",
2362
+ "sha256": "8cf43120ea485fae7dc6aae38a14a3e703dbd18422ef13a199aedb3ec66b0184",
2363
+ "mode": 493,
2364
+ "origin": "kit"
2365
+ },
2271
2366
  {
2272
2367
  "path": "scripts/workflow-advisories/capabilities.json",
2273
2368
  "kind": "doc",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikon85/agent-workflow-kit",
3
- "version": "0.25.0",
3
+ "version": "0.26.1",
4
4
  "description": "Portable AI-agent workflow skills (plan → execute → land → learn) for Claude Code & Codex — grilling, TDD, diagnosis, two-axis code review, cross-model Codex review, design & domain-modeling, plus a skill router (ask-matt). npx init/update/diff/uninstall.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,9 +1,10 @@
1
1
  /** Block shipped changes whose version or checked manifest does not match a fresh build. */
2
2
  import { execFileSync } from 'node:child_process';
3
- import { mkdtemp, readFile, rm } from 'node:fs/promises';
3
+ import { mkdir, mkdtemp, readFile, rm } from 'node:fs/promises';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { dirname, join } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
+ import { sha256 } from '../src/lib/hash.mjs';
7
8
  import { buildKit } from './build-kit.mjs';
8
9
 
9
10
  const byPath = (manifest) => new Map(manifest.files.map((entry) => [entry.path, entry.sha256]));
@@ -41,6 +42,7 @@ function bumpKind(before, after) {
41
42
  export function assessRelease(input) {
42
43
  const delta = manifestDelta(input.baseManifest, input.builtManifest);
43
44
  const checkedDrift = manifestDelta(input.checkedManifest, input.builtManifest);
45
+ const payloadDrift = manifestDelta(input.builtManifest, input.payloadManifest);
44
46
  const errors = [];
45
47
  if (hasDelta(delta) && input.currentVersion === input.baseVersion) {
46
48
  errors.push(`shipped delta has no version bump (${describe(delta)}); version remains ${input.currentVersion}`);
@@ -52,6 +54,9 @@ export function assessRelease(input) {
52
54
  if (checkedDrift.added.length || checkedDrift.changed.length) {
53
55
  errors.push(`checked manifest is stale (${describe(checkedDrift)})`);
54
56
  }
57
+ if (hasDelta(payloadDrift)) {
58
+ errors.push(`npm package payload does not match built manifest (${describe(payloadDrift)})`);
59
+ }
55
60
  const recommendedBump = recommendBump(delta);
56
61
  const actual = bumpKind(input.baseVersion, input.currentVersion);
57
62
  const rank = { patch: 1, minor: 2, major: 3 };
@@ -68,18 +73,43 @@ function gitShowJson(repoRoot, ref, path) {
68
73
  return JSON.parse(execFileSync('git', ['show', `${ref}:${path}`], { cwd: repoRoot, encoding: 'utf8' }));
69
74
  }
70
75
 
76
+ export async function packedPayloadManifest({ repoRoot, manifest }) {
77
+ const tempRoot = await mkdtemp(join(tmpdir(), 'awkit-package-payload-'));
78
+ try {
79
+ const packOutput = execFileSync(
80
+ 'npm', ['pack', '--json', '--ignore-scripts', '--pack-destination', tempRoot],
81
+ { cwd: repoRoot, encoding: 'utf8' },
82
+ );
83
+ const [{ filename }] = JSON.parse(packOutput);
84
+ const unpackDir = join(tempRoot, 'unpacked');
85
+ await mkdir(unpackDir);
86
+ execFileSync('tar', ['-xzf', join(tempRoot, filename), '-C', unpackDir]);
87
+ return {
88
+ kitVersion: manifest.kitVersion,
89
+ files: await Promise.all(manifest.files.map(async (entry) => ({
90
+ ...entry,
91
+ sha256: sha256(await readFile(join(unpackDir, 'package', entry.path))),
92
+ }))),
93
+ };
94
+ } finally {
95
+ await rm(tempRoot, { recursive: true, force: true });
96
+ }
97
+ }
98
+
71
99
  export async function checkReleaseDelta({ repoRoot, baseRef = 'origin/main' } = {}) {
72
100
  repoRoot ??= join(dirname(fileURLToPath(import.meta.url)), '..');
73
101
  const distDir = await mkdtemp(join(tmpdir(), 'awkit-release-guard-'));
74
102
  try {
75
103
  await buildKit({ repoRoot, distDir });
76
104
  const currentPackage = JSON.parse(await readFile(join(repoRoot, 'package.json'), 'utf8'));
105
+ const builtManifest = JSON.parse(await readFile(join(distDir, 'agent-workflow-kit.package.json'), 'utf8'));
77
106
  return assessRelease({
78
107
  baseVersion: gitShowJson(repoRoot, baseRef, 'package.json').version,
79
108
  currentVersion: currentPackage.version,
80
109
  baseManifest: gitShowJson(repoRoot, baseRef, 'agent-workflow-kit.package.json'),
81
110
  checkedManifest: JSON.parse(await readFile(join(repoRoot, 'agent-workflow-kit.package.json'), 'utf8')),
82
- builtManifest: JSON.parse(await readFile(join(distDir, 'agent-workflow-kit.package.json'), 'utf8')),
111
+ builtManifest,
112
+ payloadManifest: await packedPayloadManifest({ repoRoot, manifest: builtManifest }),
83
113
  });
84
114
  } finally { await rm(distDir, { recursive: true, force: true }); }
85
115
  }
@@ -1,8 +1,29 @@
1
1
  import { test } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { assessRelease } from './release-delta-guard.mjs';
3
+ import { mkdtemp, readFile, rm } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { dirname, join } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { buildKit } from './build-kit.mjs';
8
+ import {
9
+ assessRelease, manifestDelta, packedPayloadManifest,
10
+ } from './release-delta-guard.mjs';
4
11
 
5
12
  const file = (path, sha256) => ({ path, sha256 });
13
+ const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
14
+
15
+ test('fresh build manifest hashes match the actual npm package payload', async () => {
16
+ const tempRoot = await mkdtemp(join(tmpdir(), 'awkit-packed-manifest-test-'));
17
+ try {
18
+ const distDir = join(tempRoot, 'dist');
19
+ await buildKit({ repoRoot, distDir });
20
+ const built = JSON.parse(await readFile(join(distDir, 'agent-workflow-kit.package.json'), 'utf8'));
21
+ const packed = await packedPayloadManifest({ repoRoot, manifest: built });
22
+ assert.deepEqual(manifestDelta(built, packed), { added: [], removed: [], changed: [] });
23
+ } finally {
24
+ await rm(tempRoot, { recursive: true, force: true });
25
+ }
26
+ });
6
27
 
7
28
  test('an unbumped shipped change is blocked with its concrete delta', () => {
8
29
  const result = assessRelease({
@@ -10,6 +31,7 @@ test('an unbumped shipped change is blocked with its concrete delta', () => {
10
31
  baseManifest: { kitVersion: '1.2.3', files: [file('skill.md', 'old')] },
11
32
  builtManifest: { kitVersion: '1.2.3', files: [file('skill.md', 'new')] },
12
33
  checkedManifest: { kitVersion: '1.2.3', files: [file('skill.md', 'old')] },
34
+ payloadManifest: { kitVersion: '1.2.3', files: [file('skill.md', 'new')] },
13
35
  });
14
36
  assert.equal(result.ok, false);
15
37
  assert.deepEqual(result.delta.changed, ['skill.md']);
@@ -23,6 +45,7 @@ test('dead checked-manifest entries are rejected', () => {
23
45
  baseManifest: { kitVersion: '1.2.3', files: [] },
24
46
  builtManifest: { kitVersion: '1.3.0', files: [] },
25
47
  checkedManifest: { kitVersion: '1.3.0', files: [file('gone.md', 'old')] },
48
+ payloadManifest: { kitVersion: '1.3.0', files: [] },
26
49
  });
27
50
  assert.equal(result.ok, false);
28
51
  assert.match(result.errors.join('\n'), /dead entry: gone\.md/);
@@ -33,7 +56,7 @@ test('a matching regenerated manifest and semantic bump pass', () => {
33
56
  const result = assessRelease({
34
57
  baseVersion: '1.2.3', currentVersion: '1.3.0',
35
58
  baseManifest: { kitVersion: '1.2.3', files: [] }, builtManifest: current,
36
- checkedManifest: current,
59
+ checkedManifest: current, payloadManifest: current,
37
60
  });
38
61
  assert.equal(result.ok, true);
39
62
  assert.equal(result.recommendedBump, 'minor');
@@ -45,9 +68,20 @@ test('shipped content rejects downgrade and malformed version transitions', () =
45
68
  const result = assessRelease({
46
69
  baseVersion: '1.2.3', currentVersion,
47
70
  baseManifest: { kitVersion: '1.2.3', files: [file('skill.md', 'old')] },
48
- builtManifest: current, checkedManifest: current,
71
+ builtManifest: current, checkedManifest: current, payloadManifest: current,
49
72
  });
50
73
  assert.equal(result.ok, false, currentVersion);
51
74
  assert.match(result.errors.join('\n'), /invalid version transition: 1\.2\.3 ->/);
52
75
  }
53
76
  });
77
+
78
+ test('actual npm payload drift is blocked even when checked and built manifests match', () => {
79
+ const scrubbed = { kitVersion: '1.2.3', files: [file('skill.md', 'scrubbed')] };
80
+ const result = assessRelease({
81
+ baseVersion: '1.2.3', currentVersion: '1.2.3',
82
+ baseManifest: scrubbed, builtManifest: scrubbed, checkedManifest: scrubbed,
83
+ payloadManifest: { kitVersion: '1.2.3', files: [file('skill.md', 'source')] },
84
+ });
85
+ assert.equal(result.ok, false);
86
+ assert.match(result.errors.join('\n'), /npm package payload.*changed: skill\.md/);
87
+ });
@@ -124,6 +124,13 @@ def _known_hashes(root: Path) -> set:
124
124
  for f in data.get("files", []):
125
125
  if "sha256" in f:
126
126
  hashes.add(f["sha256"])
127
+ gitleaks = root / "scripts/security/gitleaks-profile.json"
128
+ if gitleaks.exists():
129
+ data = json.loads(gitleaks.read_text(encoding="utf-8"))
130
+ for platform in data.get("platforms", {}).values():
131
+ checksum = platform.get("sha256")
132
+ if isinstance(checksum, str):
133
+ hashes.add(checksum)
127
134
  return hashes
128
135
 
129
136
 
@@ -293,6 +300,14 @@ class Exemptions(unittest.TestCase):
293
300
  {"path": "x", "sha256": "f" * 64}]}), encoding="utf-8")
294
301
  self.assertEqual(audit_dir(self.dir), [])
295
302
 
303
+ def test_gitleaks_profile_checksums_are_verified_pin_metadata(self):
304
+ profile = self.dir / "scripts/security"
305
+ profile.mkdir(parents=True)
306
+ (profile / "gitleaks-profile.json").write_text(json.dumps({
307
+ "platforms": {"linux-x64": {"sha256": "e" * 64}},
308
+ }), encoding="utf-8")
309
+ self.assertEqual(audit_dir(self.dir), [])
310
+
296
311
  def test_loc_offender_traversal_guard_allowed(self):
297
312
  p = self.dir / "scripts"
298
313
  p.mkdir()
@@ -104,6 +104,20 @@ export const HELPER_FILES = [
104
104
  { path: '.claude/hooks/migration-snapshot-reminder.py', kind: 'hook', mode: 0o755 },
105
105
  { path: '.claude/hooks/loc-offender-forewarn.py', kind: 'hook', mode: 0o755 },
106
106
  { path: '.claude/hooks/drift-guard.py', kind: 'hook', mode: 0o755 },
107
+ // Counted Safety Guardrails unit: shared policy/search core, one loader,
108
+ // four thin Agent adapters, and three portable repository-security
109
+ // primitives. Activation stays consumer-owned through setup-workflow.
110
+ { path: 'scripts/safety-guardrails/core.py', kind: 'script', mode: 0o644 },
111
+ { path: 'scripts/safety-guardrails/search.py', kind: 'script', mode: 0o644 },
112
+ { path: '.claude/hooks/_safety_guard.py', kind: 'hook', mode: 0o644 },
113
+ { path: '.claude/hooks/block-secrets.py', kind: 'hook', mode: 0o755 },
114
+ { path: '.claude/hooks/block-npm-install-in-pnpm.py', kind: 'hook', mode: 0o755 },
115
+ { path: '.claude/hooks/block-bg-double-background.py', kind: 'hook', mode: 0o755 },
116
+ { path: '.claude/hooks/grep-shim-guard.py', kind: 'hook', mode: 0o755 },
117
+ { path: 'scripts/security/install-git-hooks.mjs', kind: 'script', mode: 0o755 },
118
+ { path: 'scripts/security/ensure-gitleaks.mjs', kind: 'script', mode: 0o755 },
119
+ { path: 'scripts/security/gitleaks-profile.json', kind: 'doc', mode: 0o644 },
120
+ { path: 'scripts/security/audit-gate.mjs', kind: 'script', mode: 0o755 },
107
121
  // SessionStart skill-freshness drift-hint (audit-skills names it). For each
108
122
  // <skill>/SOURCES.txt it flags sources newer in git than the SKILL.md. Imports
109
123
  // _hook_utils (shipped above); stdlib-only otherwise. Executable hook → 0o755.