actions-warden 0.2.0 → 0.3.0

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 (62) hide show
  1. package/AGENTS.md +189 -0
  2. package/CONTRIBUTING.md +109 -0
  3. package/README.md +272 -224
  4. package/RELEASING.md +338 -0
  5. package/SECURITY.md +25 -3
  6. package/docs/AI-AGENTS.md +458 -0
  7. package/docs/CLI.md +421 -0
  8. package/docs/CONFIGURATION.md +340 -0
  9. package/docs/DEVELOPMENT.md +350 -0
  10. package/docs/GITHUB-ACTION.md +281 -0
  11. package/docs/JAVASCRIPT-API.md +355 -0
  12. package/docs/OUTPUTS.md +409 -0
  13. package/docs/README.md +27 -0
  14. package/examples/org-scan.yml +42 -0
  15. package/examples/upgrade-pr.yml +57 -0
  16. package/llms.txt +38 -0
  17. package/package.json +32 -10
  18. package/skills/actions-warden/SKILL.md +140 -38
  19. package/src/action.js +317 -0
  20. package/src/cli.js +267 -22
  21. package/src/commands/audit.js +189 -36
  22. package/src/commands/org-scan.js +549 -0
  23. package/src/commands/pin.js +59 -56
  24. package/src/commands/report.js +122 -10
  25. package/src/commands/upgrade.js +102 -62
  26. package/src/commands/verify.js +193 -0
  27. package/src/index.js +21 -4
  28. package/src/lib/action-status.js +27 -0
  29. package/src/lib/agent-mode.js +175 -0
  30. package/src/lib/annotations.js +250 -0
  31. package/src/lib/baseline.js +103 -0
  32. package/src/lib/cache.js +47 -10
  33. package/src/lib/concurrency.js +27 -0
  34. package/src/lib/config.js +185 -0
  35. package/src/lib/execution.js +71 -0
  36. package/src/lib/formatter.js +127 -8
  37. package/src/lib/github-org.js +374 -0
  38. package/src/lib/identity.js +62 -0
  39. package/src/lib/ignore.js +7 -6
  40. package/src/lib/org-checkpoint.js +378 -0
  41. package/src/lib/org-progress.js +60 -0
  42. package/src/lib/parser.js +326 -52
  43. package/src/lib/patcher.js +199 -0
  44. package/src/lib/paths.js +35 -12
  45. package/src/lib/redact.js +65 -4
  46. package/src/lib/resolver.js +225 -43
  47. package/src/lib/targets.js +28 -0
  48. package/src/lib/triggers.js +12 -0
  49. package/src/lib/writer.js +45 -8
  50. package/src/rules/excessive-permissions.js +24 -33
  51. package/src/rules/index.js +19 -1
  52. package/src/rules/pull-request-target-checkout.js +149 -18
  53. package/src/rules/reusable-workflow-secrets.js +32 -0
  54. package/src/rules/script-injection.js +77 -12
  55. package/src/rules/secrets-in-env.js +101 -18
  56. package/src/rules/unpinned-action.js +3 -2
  57. package/src/rules/unpinned-container-image.js +39 -0
  58. package/src/rules/unpinned-docker-action.js +30 -0
  59. package/src/rules/untrusted-self-hosted-runner.js +109 -0
  60. package/src/rules/workflow-run-artifact-execution.js +122 -0
  61. package/src/rules/workflow-structure.js +396 -0
  62. package/src/version.js +3 -0
@@ -0,0 +1,340 @@
1
+ # Configuration, ignores, and baselines
2
+
3
+ actions-warden can run with built-in defaults, or load a versioned policy from
4
+ the repository being scanned.
5
+
6
+ ## Policy discovery
7
+
8
+ The default filenames are:
9
+
10
+ ```text
11
+ .actions-warden.yml
12
+ .actions-warden.yaml
13
+ ```
14
+
15
+ If both exist, `.actions-warden.yml` wins. Select another file with `--config`,
16
+ or skip repository policy with `--ignore-config`:
17
+
18
+ ```sh
19
+ actions-warden audit --config=security/actions-warden.yml
20
+ actions-warden audit --ignore-config
21
+ ```
22
+
23
+ The policy path must resolve inside `--cwd`, including after symlink
24
+ resolution. `--config` and `--ignore-config` are mutually exclusive.
25
+
26
+ Configuration is strict by design. Unknown top-level keys, unknown rule IDs,
27
+ unknown nested keys, invalid severities, and values of the wrong type stop the
28
+ scan. This prevents a typo from silently weakening policy.
29
+
30
+ ## Complete policy shape
31
+
32
+ ```yaml
33
+ version: 1
34
+ baseline: .actions-warden-baseline.json
35
+
36
+ ignore-paths:
37
+ - .github/workflows/generated/**
38
+ - .github/actions/vendor/**
39
+
40
+ rules:
41
+ excessive-permissions:
42
+ enabled: true
43
+ severity: high
44
+ unpinned-container-image:
45
+ enabled: false
46
+
47
+ runner-policy:
48
+ self-hosted-labels:
49
+ - private-*
50
+ - arc-*
51
+ trusted-groups:
52
+ - github-hosted-*
53
+ flag-unknown-groups: true
54
+ ```
55
+
56
+ | key | type | default | meaning |
57
+ |---|---|---|---|
58
+ | `version` | integer | `1` | Policy schema; if present, must be `1` |
59
+ | `baseline` | non-empty string | none | Baseline path relative to `--cwd` |
60
+ | `ignore-paths` | string array | `[]` | Picomatch globs excluded before parsing |
61
+ | `rules` | mapping | `{}` | Per-rule enablement and severity overrides |
62
+ | `runner-policy` | mapping | built-in defaults | Additional self-hosted runner selectors and group trust |
63
+
64
+ Rule policies accept only:
65
+
66
+ | key | type | default |
67
+ |---|---|---|
68
+ | `enabled` | boolean | `true` |
69
+ | `severity` | `low`, `medium`, `high`, or `critical` | rule default |
70
+
71
+ A CLI `--baseline` overrides the policy's `baseline`. A CLI `--severity`
72
+ filters after any configured severity override.
73
+
74
+ ## Rule IDs
75
+
76
+ Use the installed version as the source of truth:
77
+
78
+ ```sh
79
+ actions-warden rules --format=json
80
+ ```
81
+
82
+ The current built-in rules are:
83
+
84
+ | ID | default severity | detects |
85
+ |---|---|---|
86
+ | `unpinned-action` | high | External actions and reusable workflows without a full commit SHA |
87
+ | `unpinned-docker-action` | high | Mutable `docker://` action images |
88
+ | `unpinned-container-image` | high | Mutable job, service, or Docker action images |
89
+ | `excessive-permissions` | medium | Broadly writable workflow or job `GITHUB_TOKEN` permissions |
90
+ | `secrets-in-env` | medium | Named job secrets exposed broadly; workflow-wide or dynamic all-secret exposure is high |
91
+ | `script-injection` | critical | Untrusted GitHub context, including tainted `env`, interpolated into a shell or script action |
92
+ | `pull-request-target-checkout` | critical | Privileged pull-request code retrieval without an active checkout guard |
93
+ | `reusable-workflow-secrets-inherit` | high | External reusable workflows receiving every caller secret |
94
+ | `untrusted-self-hosted-runner` | high | Untrusted pull-request code reaching self-hosted infrastructure |
95
+ | `workflow-run-artifact-execution` | critical | Privileged `workflow_run` jobs executing cross-run artifacts |
96
+ | `workflow-structure` | medium | Structurally invalid workflow or composite-action syntax |
97
+
98
+ Rule findings may adjust severity based on the recognized pattern. A configured
99
+ severity override is applied to every finding produced by that rule.
100
+
101
+ ## Rule accuracy and analysis boundaries
102
+
103
+ The audit is static and offline. It parses YAML and follows a small amount of
104
+ local data flow, but it does not evaluate expressions, call referenced actions,
105
+ inspect organization settings, or execute workflow code. Findings therefore
106
+ describe a recognized risk pattern and its evidence, not a proof that every
107
+ runtime path is exploitable.
108
+
109
+ The current rule boundaries are deliberate:
110
+
111
+ - `unpinned-action` requires a full 40-character commit SHA for external
112
+ actions and reusable workflows. Workspace-relative `./` references and
113
+ GitHub's `$/` self-repository references are not external dependencies. A
114
+ `$/` reference follows the exact workflow commit and requires runner 2.336.0
115
+ or newer, cannot include an `@ref` suffix, and is not available on GitHub
116
+ Enterprise Server; see GitHub's [self-repository syntax announcement](https://github.blog/changelog/2026-07-30-reference-same-repository-actions-with-self-repository-syntax/).
117
+ - `excessive-permissions` treats a missing top-level declaration as a low
118
+ advisory because token defaults are configurable. It reports `write-all` and
119
+ maps with at least three writable scopes, but does not guess that one
120
+ purpose-specific write scope is unnecessary. Invalid permission values are
121
+ reported by `workflow-structure` instead.
122
+ - `secrets-in-env` reports a named secret at job scope as medium, and raises
123
+ workflow-wide scope, dynamic indexing, and `toJSON(secrets)` to high. GitHub
124
+ may need to expose every available secret to resolve a dynamic reference.
125
+ Job scope can be necessary for a secret-dependent `if` condition; in that
126
+ case every job step should be trusted and the secret should carry minimal
127
+ privilege. A named step-scoped secret is outside this rule, but dynamic or
128
+ all-secret access is reported even at step scope.
129
+ - `script-injection` follows known attacker-controlled GitHub fields directly
130
+ into `run` and `actions/github-script`, and through workflow, job, or step
131
+ `env` when the script re-interpolates `${{ env.NAME }}`. The safe boundary is
132
+ native shell syntax such as `"$NAME"` or JavaScript `process.env.NAME`, as in
133
+ GitHub's [CodeQL recommendation](https://codeql.github.com/codeql-query-help/actions/actions-code-injection-critical/).
134
+ - `pull-request-target-checkout` understands GitHub's June 2026 checkout
135
+ protection and its July 20 backport for floating `actions/checkout` v2-v7
136
+ tags, protected release
137
+ versions (`v2.8.0`, `v3.7.0`, `v4.4.0`, `v5.1.0`, `v6.1.0`, and v7+), and the
138
+ exact known release SHAs. It still reports an explicit or expression-driven
139
+ `allow-unsafe-pr-checkout`, older versions, direct `git`/`gh` fetches, and
140
+ unknown pinned SHAs. Version comments are not trusted as security evidence.
141
+ A protected floating major can therefore avoid this finding while still
142
+ producing `unpinned-action`; the two rules cover different risks. See
143
+ GitHub's [checkout protection announcement](https://github.blog/changelog/2026-06-18-safer-pull_request_target-defaults-for-github-actions-checkout/).
144
+ - `workflow-run-artifact-execution` treats the official
145
+ `actions/download-artifact` action as cross-run only when `run-id` selects
146
+ another run. A same-run download is not enough to trigger the rule. Artifact
147
+ paths under `${{ runner.temp }}` remain isolated from unrelated workspace
148
+ commands, but execution or loading from that exact path is still reported.
149
+ - `untrusted-self-hosted-runner` reports ordinary `pull_request` jobs on
150
+ recognized self-hosted selectors. For `pull_request_target`, it requires a
151
+ recognized unsafe PR checkout or direct fetch instead of assuming that a
152
+ trusted labeling job executes fork code. Literal matrix dimensions and
153
+ `include` entries are followed when `runs-on` selects a matrix key. Matrices
154
+ generated by expressions still require manual review. Use `runner-policy`
155
+ for organization-specific labels and groups.
156
+ - `workflow-structure` checks required trigger/job/step shapes, reusable-job
157
+ conflicts, permission values, and composite run-step shells. It recognizes
158
+ GitHub's current `background`, `wait`, `wait-all`, `cancel`, and `parallel`
159
+ step syntax, validates the documented control shapes, and recursively scans
160
+ actions and scripts inside `parallel` groups. Background and parallel steps
161
+ authored inside composite actions are reported because GitHub does not
162
+ support them there. See GitHub's [current concurrent-step
163
+ syntax](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idstepsbackground).
164
+ This rule is not a full replacement for GitHub's server-side workflow
165
+ validator.
166
+
167
+ Execution recognition is intentionally conservative but finite: it covers
168
+ common shell scripts, package managers, build/test commands, local actions, and
169
+ build-like third-party actions. A custom action or unusual interpreter can
170
+ consume untrusted files without being recognized. Review privileged
171
+ `pull_request_target` and `workflow_run` jobs manually, and use GitHub Actions
172
+ CodeQL alongside this audit when complete interprocedural analysis is needed.
173
+
174
+ ## Runner policy
175
+
176
+ The self-hosted runner rule always recognizes the literal `self-hosted` label.
177
+ Policy can add organization-specific labels and group handling:
178
+
179
+ ```yaml
180
+ runner-policy:
181
+ self-hosted-labels:
182
+ - linux-prod-*
183
+ - arc-runner-*
184
+ trusted-groups:
185
+ - github-hosted-*
186
+ flag-unknown-groups: true
187
+ ```
188
+
189
+ `self-hosted-labels` and `trusted-groups` use Picomatch glob syntax.
190
+
191
+ GitHub runner groups are not assumed unsafe by default because a group may
192
+ select GitHub-hosted infrastructure. When `flag-unknown-groups` is `true`, a
193
+ pull-request job using a group is reported unless the group matches
194
+ `trusted-groups`.
195
+
196
+ ## Path exclusions
197
+
198
+ `ignore-paths` is applied before a workflow is parsed:
199
+
200
+ ```yaml
201
+ ignore-paths:
202
+ - .github/workflows/generated/**
203
+ - .github/actions/third-party/**
204
+ ```
205
+
206
+ For a local audit, paths are relative to `--cwd`. For `org-scan`, the local
207
+ organization policy applies to every repository and paths begin with
208
+ `owner/repository/`:
209
+
210
+ ```yaml
211
+ ignore-paths:
212
+ - my-org/generated-repo/.github/workflows/**
213
+ - my-org/archive-*/.github/actions/**
214
+ ```
215
+
216
+ A path exclusion removes the file from coverage. Use it for generated or
217
+ vendored sources whose ownership and review process is understood, and protect
218
+ policy changes like code.
219
+
220
+ ## Inline ignore directives
221
+
222
+ Use an inline directive when a specific finding has been reviewed and the file
223
+ should remain in scan coverage.
224
+
225
+ Ignore one rule on the same line:
226
+
227
+ ```yaml
228
+ - uses: owner/action@v1 # actions-warden-ignore: unpinned-action
229
+ ```
230
+
231
+ Ignore the next non-empty, non-comment line:
232
+
233
+ ```yaml
234
+ # actions-warden-ignore-next-line: excessive-permissions
235
+ permissions: write-all
236
+ ```
237
+
238
+ Ignore a block:
239
+
240
+ ```yaml
241
+ # actions-warden-ignore-start: workflow-structure
242
+ permissions:
243
+ # actions-warden-ignore-end
244
+ ```
245
+
246
+ Ignore a rule for the whole file:
247
+
248
+ ```yaml
249
+ # actions-warden-ignore-file: unpinned-action
250
+ ```
251
+
252
+ The short `aw-` prefix is also accepted, for example
253
+ `# aw-ignore-next-line: script-injection`. Multiple rule IDs may be separated by
254
+ commas or whitespace. Omitting rule IDs suppresses every rule in that scope;
255
+ prefer naming the narrowest rule so future checks remain active.
256
+
257
+ Unmatched `ignore-start` directives extend to the end of the file. An
258
+ `ignore-end` without an open block has no effect.
259
+
260
+ ## Baselines
261
+
262
+ A baseline accepts existing, reviewed findings while allowing new findings to
263
+ fail the scan.
264
+
265
+ Create one from the current unfiltered finding set:
266
+
267
+ ```sh
268
+ actions-warden audit \
269
+ --create-baseline=.actions-warden-baseline.json
270
+ ```
271
+
272
+ The command writes a deterministic document:
273
+
274
+ ```json
275
+ {
276
+ "schemaVersion": "1.0",
277
+ "generatedBy": "actions-warden",
278
+ "findings": [
279
+ {
280
+ "id": "18b82e86d7c14fe2",
281
+ "fingerprint": "a5c4f4a2797d17d9",
282
+ "ruleId": "unpinned-action",
283
+ "severity": "high",
284
+ "file": ".github/workflows/ci.yml",
285
+ "line": 14
286
+ }
287
+ ]
288
+ }
289
+ ```
290
+
291
+ Load it explicitly:
292
+
293
+ ```sh
294
+ actions-warden audit \
295
+ --baseline=.actions-warden-baseline.json
296
+ ```
297
+
298
+ or through policy:
299
+
300
+ ```yaml
301
+ version: 1
302
+ baseline: .actions-warden-baseline.json
303
+ ```
304
+
305
+ Each finding has two matching identities:
306
+
307
+ - `id` identifies the exact repository-relative source occurrence and is stable
308
+ across clones;
309
+ - `fingerprint` ignores line movement while retaining semantic fields, file,
310
+ rule, and source-order ordinal for equivalent duplicates.
311
+
312
+ A baseline match suppresses the finding from `findings`. The summary still
313
+ reports `totalFindings` and `suppressed`. Parser failures cannot be baselined.
314
+
315
+ Review baseline diffs carefully. Deleting a finding from the baseline makes it
316
+ active again; adding one accepts that risk without changing the workflow.
317
+
318
+ ## Policy ownership
319
+
320
+ Treat these files as security controls:
321
+
322
+ ```text
323
+ .actions-warden.yml
324
+ .actions-warden.yaml
325
+ .actions-warden-baseline.json
326
+ ```
327
+
328
+ Recommended controls include:
329
+
330
+ - require review from a security or platform owner through `CODEOWNERS`;
331
+ - prevent unreviewed direct pushes with branch protection;
332
+ - show policy and baseline diffs in dependency-maintenance pull requests;
333
+ - avoid granting a scanning pull request permission to rewrite its own policy.
334
+
335
+ ## Related guides
336
+
337
+ - [CLI reference](./CLI.md)
338
+ - [Output contracts](./OUTPUTS.md)
339
+ - [GitHub Action](./GITHUB-ACTION.md)
340
+ - [Security policy](../SECURITY.md)
@@ -0,0 +1,350 @@
1
+ # Developer guide
2
+
3
+ This guide is for contributors changing actions-warden itself. For public CLI
4
+ usage, start with the [CLI reference](./CLI.md).
5
+
6
+ ## Prerequisites
7
+
8
+ - Node.js 20 or newer;
9
+ - npm with the lockfile committed by the repository;
10
+ - optional: `prek` or Python `pre-commit` for local hooks.
11
+
12
+ Install exactly the locked dependency graph:
13
+
14
+ ```sh
15
+ npm ci
16
+ ```
17
+
18
+ The repository's `.npmrc` pins dependencies exactly and disables package
19
+ lifecycle scripts during installation.
20
+
21
+ Install hooks with either runner:
22
+
23
+ ```sh
24
+ prek install --hook-type pre-commit --hook-type pre-push
25
+ ```
26
+
27
+ or:
28
+
29
+ ```sh
30
+ pre-commit install --hook-type pre-commit
31
+ pre-commit install --hook-type pre-push
32
+ ```
33
+
34
+ ## Validation commands
35
+
36
+ | command | purpose |
37
+ |---|---|
38
+ | `npm test` | Run the complete Vitest suite once |
39
+ | `npm run test:watch` | Run Vitest in watch mode |
40
+ | `npm run lint` | Lint source, scripts, and tests |
41
+ | `npm run check:yaml` | Parse repository YAML files |
42
+ | `npm run check:docs` | Validate local documentation links |
43
+ | `npm run check:package` | Inspect the exact npm tarball manifest, required files, executable mode, and size bounds |
44
+ | `npm run verify-deps` | Require exact runtime and development versions |
45
+ | `npm run verify-version-sync` | Keep package, lockfile, bundled runtime, plugin, and public invocations aligned |
46
+ | `npm run build:action` | Rebuild the committed Action bundle |
47
+ | `npm run check:action-bundle` | Compare a clean rebuild with working and staged bundles |
48
+ | `npm run audit` | Run the configured npm vulnerability threshold |
49
+ | `npm run release:prepare -- X.Y.Z` | Update all local stable-version sources without committing or publishing |
50
+ | `npm run release:check` | Gate a staged release candidate against npm state and every release validation |
51
+
52
+ Before opening a pull request, run:
53
+
54
+ ```sh
55
+ npm run verify-version-sync
56
+ npm run verify-deps
57
+ npm run check:yaml
58
+ npm run check:docs
59
+ npm run check:package
60
+ npm run lint
61
+ npm test
62
+ npm run audit
63
+ ```
64
+
65
+ If code reachable from `src/action.js` changed, also run:
66
+
67
+ ```sh
68
+ npm run build:action
69
+ npm run check:action-bundle
70
+ ```
71
+
72
+ ## Repository map
73
+
74
+ ```text
75
+ src/
76
+ cli.js CLI parsing, exit codes, and output routing
77
+ action.js GitHub Action input/output adapter
78
+ index.js public JavaScript exports
79
+ version.js runtime version embedded in the Action bundle
80
+ commands/ audit, pin, verify, upgrade, report, org-scan
81
+ lib/
82
+ parser.js YAML-to-normalized-workflow model
83
+ targets.js, paths.js repository target discovery
84
+ config.js, baseline.js policy and accepted-finding controls
85
+ resolver.js, cache.js GitHub API, caching, ref and ownership checks
86
+ github-org.js read-only organization/tree/blob access
87
+ agent-mode.js explicit bounded AI-agent CLI defaults
88
+ org-checkpoint.js validated atomic organization resume state
89
+ org-progress.js human rendering of structured scan progress
90
+ identity.js stable IDs and semantic fingerprints
91
+ patcher.js, writer.js source-range rewrites and guarded atomic writes
92
+ formatter.js, redact.js structured output and credential redaction
93
+ annotations.js GitHub workflow annotations
94
+ concurrency.js bounded async work
95
+ rules/ one module per audit rule
96
+
97
+ test/ Vitest suites and hostile/edge-case fixtures
98
+ scripts/ repository validation and release helpers
99
+ docs/ user, integration, AI, and developer guides
100
+ examples/ copyable GitHub workflow examples
101
+ skills/actions-warden/ Claude Code skill
102
+ dist/ generated, committed GitHub Action bundle
103
+ action.yml public Action inputs, outputs, and runtime
104
+ ```
105
+
106
+ ## Data flow
107
+
108
+ A local audit follows this path:
109
+
110
+ ```text
111
+ targets → source read → YAML parser → normalized workflow model
112
+ → ignore directives → rules → stable identities
113
+ → severity/baseline filtering → renderer
114
+ ```
115
+
116
+ Pin and upgrade add:
117
+
118
+ ```text
119
+ normalized action refs → GitHub resolution and ownership verification
120
+ → source-range patch plan → reparse
121
+ → guarded atomic writer only when dryRun=false
122
+ ```
123
+
124
+ An organization scan follows:
125
+
126
+ ```text
127
+ repository listing → default-branch Git tree → bounded YAML blob reads
128
+ → in-memory auditSources → repository aggregation
129
+
130
+ optional checkpoint → identity validation → fresh tree SHA comparison
131
+ → reuse unchanged error-free result or rescan
132
+ ```
133
+
134
+ Remote repositories are never cloned, checked out, imported, or executed.
135
+
136
+ ## Safety invariants
137
+
138
+ Changes must preserve these boundaries:
139
+
140
+ - `pin` and `upgrade` default to dry-run at the command and API layers.
141
+ - CLI and Action mutation require an explicit write option.
142
+ - A resolver failure must not fall back to a guessed tag or SHA.
143
+ - Commits must be verified as belonging to the referenced repository.
144
+ - Workflow rewrites operate on parsed scalar ranges and reparse before write.
145
+ - Writes and configured paths remain inside the real repository root.
146
+ - Symlink escapes and explicit targets that match nothing fail.
147
+ - Credentials are redacted in JSON, TOON, text, SARIF, annotations, and
148
+ top-level errors.
149
+ - Finding and change IDs exclude absolute checkout paths.
150
+ - Parser errors remain findings and cannot be baselined away.
151
+ - Organization coverage failures remain visible and make status fail.
152
+ - Organization source reads remain count- and size-bounded and bypass disk
153
+ cache.
154
+ - Organization resume never trusts a stale result without fresh discovery and
155
+ a matching repository identity, default branch, and tree SHA. Failed results
156
+ are never reused.
157
+ - Checkpoints use guarded atomic writes, remain inside the working directory,
158
+ omit tokens and raw YAML, and cannot replace active policy or baseline files.
159
+ - Progress stays outside deterministic report serialization; CLI progress is
160
+ stderr-only and Action progress cannot form workflow commands.
161
+ - Agent mode is an explicit opt-in, never a TTY, parent-process, CI, or
162
+ vendor-environment heuristic. Explicit CLI output and progress options win.
163
+ Its automatic artifact key uses the validated checkpoint identity, and its
164
+ stdout receipt never includes findings or repository result arrays.
165
+ - Attacker-controlled values cannot forge TOON lines or GitHub annotations.
166
+
167
+ Tests should demonstrate the fail-closed behavior for any change touching these
168
+ boundaries.
169
+
170
+ ## Adding or changing a rule
171
+
172
+ A rule module exports:
173
+
174
+ ```js
175
+ export const id = 'example-rule';
176
+ export const severity = 'high';
177
+ export const description = 'Short sentence describing the risk.';
178
+
179
+ export function check(workflow, context = {}) {
180
+ return [{
181
+ id,
182
+ severity,
183
+ line: 1,
184
+ fields: {
185
+ type: id,
186
+ sev: severity,
187
+ evidence: 'structured-value',
188
+ },
189
+ explain: 'one concise remediation',
190
+ }];
191
+ }
192
+ ```
193
+
194
+ Then:
195
+
196
+ 1. register the module in `src/rules/index.js`;
197
+ 2. add focused tests for safe and unsafe cases;
198
+ 3. test trigger, expression, quoting, and malformed-input boundaries relevant
199
+ to the rule;
200
+ 4. when behavior depends on a moving GitHub or first-party action contract,
201
+ verify it against a primary source, record the date/version boundary in code,
202
+ and test the last unsafe plus first safe version;
203
+ 5. test at least one legitimate pattern that must remain clean so remediation
204
+ guidance does not become a false-positive generator;
205
+ 6. update the rule table and accuracy boundaries in `docs/CONFIGURATION.md`,
206
+ plus the Claude skill;
207
+ 7. run `actions-warden rules --format=json` and ensure the ID is not redacted;
208
+ 8. rebuild the Action bundle.
209
+
210
+ Keep `fields` structured and concise. They become JSON evidence, TOON fields,
211
+ SARIF messages, and Action annotations. Never place raw secret values in them.
212
+ Suggestions must name a safe end state, preserve legitimate use cases, and
213
+ avoid claiming that one mitigation closes risks outside the rule's evidence.
214
+
215
+ ## Adding or changing a command
216
+
217
+ A command normally needs changes in:
218
+
219
+ 1. `src/commands/<name>.js` for the API result and renderer;
220
+ 2. `src/cli.js` for public CLI flags and exit semantics;
221
+ 3. `src/index.js` and `package.json` exports;
222
+ 4. `src/action.js` and `action.yml` if the Action exposes it;
223
+ 5. command, CLI, Action, annotation, and output-format tests;
224
+ 6. `docs/CLI.md`, `docs/OUTPUTS.md`, API docs, and relevant examples;
225
+ 7. the Claude skill and version-sync checker when its command surface changes;
226
+ 8. the committed Action bundle.
227
+
228
+ Return operational problems as structured `errors` when useful work can
229
+ continue. Throw when top-level input or discovery makes the requested scope
230
+ undefined or unsafe.
231
+
232
+ Every non-JSON renderer must expose enough information to explain a `FAIL`
233
+ status. Every JSON renderer must include `schemaVersion`, command data, and
234
+ top-level `status`.
235
+
236
+ ## Parser and patcher changes
237
+
238
+ The parser retains both a normalized model for rules and source locations for
239
+ precise rewrites. When changing it:
240
+
241
+ - cover workflow files and composite `action.yml` metadata;
242
+ - preserve and recursively inspect steps inside `parallel` groups, while
243
+ retaining background/control-step declarations for structure checks;
244
+ - include quoted, unquoted, inline, multiline, and malformed YAML cases;
245
+ - avoid evaluating expressions;
246
+ - preserve source offsets for patchable `uses` values;
247
+ - test Windows and POSIX path normalization where relevant.
248
+
249
+ The patcher should make the smallest source-range replacement. Do not reserialize
250
+ the entire YAML document: that would destroy comments, formatting, anchors, and
251
+ reviewable diffs.
252
+
253
+ Property tests in `test/patcher-properties.test.js` exercise rewrite stability.
254
+
255
+ ## GitHub API changes
256
+
257
+ All runtime requests belong in the resolver or organization GitHub layer and
258
+ must remain pinned to `https://api.github.com`.
259
+
260
+ For new endpoints:
261
+
262
+ - validate status and response shape;
263
+ - paginate boundedly;
264
+ - validate identities, SHAs, sizes, encodings, and UTF-8 as applicable;
265
+ - set timeouts and bounded retries through the shared fetch layer;
266
+ - preserve authentication-isolated cache keys;
267
+ - decide explicitly whether private content may be cached;
268
+ - accumulate scoped errors when continued reporting is safe;
269
+ - add tests with mocked responses for truncation and malformed data.
270
+
271
+ Never execute content to determine what it contains.
272
+
273
+ When changing organization resume behavior, cover interrupted writes,
274
+ checkpoint schema and identity mismatches, hostile fields, changed tree SHAs,
275
+ previous repository errors, output equivalence, and concurrent completions.
276
+ Persist each completed result before emitting its completion event so an
277
+ observer failure or process interruption loses at most in-flight work.
278
+
279
+ ## Output compatibility
280
+
281
+ JSON is the public machine interface. Raw command results and serialized JSON
282
+ are deliberately different: renderers add repository-relative paths,
283
+ redaction, and `schemaVersion`.
284
+
285
+ When changing output:
286
+
287
+ - retain top-level `status`;
288
+ - treat new fields as additive where possible;
289
+ - update `docs/OUTPUTS.md`;
290
+ - test all four formats;
291
+ - prevent embedded control characters from forging records;
292
+ - ensure error details are visible in every format;
293
+ - verify stable IDs remain stable unless their semantic input changed.
294
+
295
+ ## GitHub Action bundle
296
+
297
+ Never edit `dist/index.js` or `dist/package.json` by hand. They are generated:
298
+
299
+ ```sh
300
+ npm run build:action
301
+ ```
302
+
303
+ Commit source and its rebuilt bundle together when the Action runtime changes.
304
+ `npm run check:action-bundle` performs a clean build in a temporary directory
305
+ and compares both working-tree and staged bundle files.
306
+
307
+ A docs-only, test-only, or CLI-only change that is unreachable from
308
+ `src/action.js` does not require a bundle rebuild.
309
+
310
+ ## Tests and fixtures
311
+
312
+ Tests use Vitest and should be deterministic and network-independent. Mock
313
+ GitHub responses rather than relying on live repositories. Use temporary
314
+ directories for writes and assert both file content and failure behavior.
315
+
316
+ Useful focused runs:
317
+
318
+ ```sh
319
+ npx vitest --run test/audit.test.js
320
+ npx vitest --run test/org-scan.test.js
321
+ npx vitest --run -t "stable id"
322
+ ```
323
+
324
+ Keep hostile input in fixtures or inline strings when it clarifies the threat
325
+ being tested. Tests should not contain real credentials.
326
+
327
+ ## Documentation maintenance
328
+
329
+ README is the landing page, not the full manual. Put durable detail in the
330
+ focused guide that owns it, then link from README.
331
+
332
+ When CLI, Action, config, output, or API behavior changes:
333
+
334
+ 1. update the owning reference document;
335
+ 2. update examples and AI guidance that depend on it;
336
+ 3. run `npm run check:docs`;
337
+ 4. run live `--help` and at least one documented example against
338
+ `node src/cli.js`.
339
+
340
+ Keep examples copyable, use exact package versions for `npx`, and use full
341
+ commit placeholders for the Action itself.
342
+
343
+ ## Releases
344
+
345
+ The authoritative [release runbook](../RELEASING.md) defines maintainer and
346
+ agent authorization, SemVer selection, live-state preflight, preparation,
347
+ trusted publication, monitoring, verification, and partial-failure recovery.
348
+ Do not bump versions as part of an unrelated contribution. `release:prepare`
349
+ is local-only; `release:check` is intended for a reviewed, staged candidate and
350
+ will fail a stale or already-published version.