Security Audit
Purpose
This skill reads and reports. It never writes. No file edit, no gh api write, no
GitHub issue, no invocation of another skill on the user's behalf. It detects security gaps
across whichever areas a repository exposes, aggregates them into one severity-ordered
report, and hands off. Every gh api command it produces is report text for a human or a
downstream skill to run — the audit never executes one.
maintain:repo-hardening is the write half of the pair. It applies the baseline via
gh api: rulesets, Actions token defaults, security features, immutable releases,
branch-restricted environments for deploy secrets. Findings from this skill are its input;
run it after an audit, or standalone on a new repo. If the user wants changes made rather
than gaps listed, that is the skill to use.
A new audit area adds a checklist under references/ and a prompt in
references/subagent-prompts.md, never a mutation — that is what keeps the skill safe to run
on a repo the user does not own.
Audit areas currently implemented:
| Area | Checklist |
|---|---|
GitHub Actions workflows under .github/workflows/ |
references/actions-checklist.md |
Release configuration (release.yml, package.json, dependabot.yml, npm publishing) |
references/release-checklist.md |
| Repository settings via the GitHub REST API (Actions defaults, rulesets, security features, immutable releases, deploy environments) | references/repo-settings-checklist.md |
| Package manager and install-time controls, pnpm and bun only (lifecycle-script allowlists, release-age gates, scope→registry mapping) | references/package-manager-checklist.md |
| Application source code (injection and unsafe-execution vectors) | references/code-security-checklist.md |
Not yet implemented: non-GitHub CI providers (CircleCI, GitLab CI), container security (Dockerfiles, base-image pinning), committed-secret scanning beyond GitHub's native feature, runtime config (CSP, CORS, security headers), dependency policy (license allowlist, banned packages).
For dependency CVE scanning specifically, use gh dependabot alerts, npm audit, or
pnpm audit directly — this skill does not duplicate that.
Boundary with audit:ci-audit. Both read .github/workflows/, split by consequence. This skill
owns the security surface: action pinning, permissions:, persist-credentials,
pull_request_target, OIDC, third-party action sources, and cache poisoning or cross-branch
scope. ci-audit owns performance, spend, and gating correctness — the job graph, cache hit
rate, matrix design, timeout-minutes, artifact retention, and concurrency. Action pinning is
graded here and only here, so the same line of YAML never gets two severities. When ci-audit finds
a required check name that no workflow produces, it hands the observation over, because reconciling
rulesets against triggers needs the gh api calls only this skill makes.
When to Use
Trigger phrases: security audit, supply chain, ci hardening, harden actions, audit release, harden settings, audit rulesets, audit install scripts, audit package manager, minimum release age, allowBuilds, staged publishing, pwn request, sha pinning.
Not for:
- Applying the fixes →
maintain:repo-hardening - Bug hunting on a diff →
review:changes-review; style and comment noise →review:code-cleanup - CI speed/parallelization →
audit:ci-audit - Dependency CVE scanning → run
gh dependabot alerts/pnpm auditdirectly
Asking the User
Every question in this skill is written as AskUserQuestion options. Use that tool where
the host offers it, or the host's nearest structured-choice equivalent. Where the host has
neither, ask the same question in normal chat as a numbered list of 2–5 options —
recommended first, one short line of description each — and wait for the user to reply
with a number.
Workflow
Step 1: Detect Repo Layout
Run in parallel. Every command here is a read:
ls .github/workflows/ 2>/dev/null
cat .github/dependabot.yml 2>/dev/null
jq '{name, packageManager, trustedDependencies, scripts: (.scripts | keys), publishConfig, private}' package.json 2>/dev/null
ls package-lock.json pnpm-lock.yaml yarn.lock bun.lock bun.lockb 2>/dev/null
cat pnpm-workspace.yaml 2>/dev/null
cat bunfig.toml 2>/dev/null
cat .npmrc 2>/dev/null
gh api repos/<owner>/<repo>/actions/permissions 2>/dev/null
gh api repos/<owner>/<repo>/actions/permissions/workflow 2>/dev/null
gh api repos/<owner>/<repo>/rulesets 2>/dev/null
gh api repos/<owner>/<repo>/immutable-releases 2>/dev/null
gh api repos/<owner>/<repo> --jq '{private, default_branch, security_and_analysis}' 2>/dev/null
Resolve <owner>/<repo> from gh repo view --json nameWithOwner --jq .nameWithOwner. If the API calls fail with 404 or auth errors, note it — repo-settings findings will be skipped for that area.
Compute the following in the orchestrator and substitute them into the dispatch prompts.
Org-scope context, for Subagent D:
org_prefix— derived frompackage.jsonname:. If the name is@<prefix>/<pkg>, the prefix is<prefix>. If the name is unscoped, leaveorg_prefixempty (no org scopes will be flagged).org_scope_patterns— given a non-emptyorg_prefix, owned scopes are@<prefix>/*AND any scope matching@<prefix>-*/*(e.g., forenonic:@enonic/*,@enonic-types/*,@enonic-cli/*).already_covered_scopes— union of:minimumReleaseAgeExcludefrompnpm-workspace.yaml(useyq),minimumReleaseAgeExcludesfrombunfig.toml,registries:keys frompnpm-workspace.yaml,[install.scopes]keys frombunfig.toml, and@<scope>:registry=lines from.npmrc.
These pre-approve scopes the team has already decided to trust. Subagent D treats them as covered and emits no finding for missing config.
Default posture: deny everything. Subagent D flags missing exclude/registry config only for owned scopes (matching org_scope_patterns) that appear in the lockfile and are not in already_covered_scopes. All other scopes stay under the release-age and dependency-confusion gates — that is the intended hardening. If org_prefix is empty, no scope-specific findings are emitted.
Release-target context, for Subagent B:
release_publishes_to_npm—trueif any of these exist (the audit does not care WHICH non-npm publisher is used, only whether npm is involved): arelease.yml/release.yamlin.github/workflows/containing a literalnpm publish,npm stage publish,pnpm publish,yarn publish, orbun publish; apublishConfig.registryfield inpackage.json; or ascripts.publishvalue containing any of those commands.detected_build_systems— assembled from root file presence:npmifpackage.json+ any lockfile,gradleifbuild.gradle/build.gradle.kts,mavenifpom.xml,cargoifCargo.toml,pipifrequirements.txt/pyproject.toml,dockerifDockerfile.github-actionsis always added when.github/workflows/exists. This drives item 7 (dependabot ecosystem coverage) so the audit recommends the right ecosystems regardless of stack.
Close the step with one line: Layout: pnpm 10.28, 5 workflows, release.yml present, gh api 200 — dispatching A, B, C, D; E skipped (config-only repo).
Routing. Skip the Actions subagent if .github/workflows/ is absent. Skip the Release subagent unless one of these holds: a release.yml/release.yaml exists, package.json has publishConfig, or package.json scripts has any of publish, release, prepublishOnly. If package.json is absent or private: true, note it — release findings about provenance change severity. Dispatch the Package Manager subagent if any lockfile exists — including package-lock.json or yarn.lock, where its whole job is the one out-of-scope finding (the audit supports pnpm and bun only) rather than a tuning pass. Dispatch the Repo Settings subagent if gh api .../actions/permissions returns HTTP 200; skip only on 404 or auth error. An empty [] from /rulesets is success — Subagent C treats the absence of rulesets as a finding, not a reason to skip. Skip the code subagent on a configuration-only or docs-only repo.
Step 2: Dispatch Audit Subagents in Parallel
Read references/subagent-prompts.md. It holds one prompt per audit area, the placeholder
substitution table, and the rule that no subagent mutates anything.
Dispatch one subagent per applicable audit area, concurrently if the host allows it. If
the host has no subagent facility, run each area sequentially inline using the same prompts.
Substitute every placeholder before dispatch — {{SKILL_DIR}} in particular, since
subagents are spawned with the user's CWD and relative checklist paths will not otherwise
resolve.
As new audit areas are added under references/, add the matching prompt to that file. No
change to Step 3 or Step 4 is needed.
Close the step with one line: Dispatched 4 of 5 auditors in parallel; awaiting A, B, C, D.
Step 3: Aggregate Findings
Wait for all dispatched subagents. Apply the four aggregation rules, then assemble the
report. Severities come from references/severity-table.md — use it to reconcile any
disagreement between subagents on the same finding shape.
Aggregation rule 1 — Pin-status cross-check (A ↔ C). Reconcile Subagent A's pin findings with Subagent C's sha_pinning_required report:
- A reports zero pin findings (every
uses:is SHA-pinned) AND C reportssha_pinning_required: false→ emit high: "All actions are SHA-pinned butsha_pinning_requiredis off. Enable to prevent regression:gh api -X PUT /repos/<owner>/<repo>/actions/permissions -F sha_pinning_required=true." - A has pin findings AND C reports
sha_pinning_required: false→ do NOT additionally flag the setting. The issue is the unpinned actions (already in A's report). In### Already hardenedadd: "sha_pinning_requiredcannot be enabled until A's pin findings are fixed — re-run the audit afterward." - C reports
sha_pinning_required: true→ "Already hardened:sha_pinning_requiredenforced."
Aggregation rule 2 — Required-status-check coverage (A ↔ C). For each {ruleset_id, protected_branches, required_checks} entry in C's RULESET_REQUIRED_CHECKS map, for each required_check, for each protected_branch:
- Find workflow(s) in A's
WORKFLOW_TRIGGER_MAPwhosename:matchesrequired_check. - If no workflow matches the name, OR none of the matching workflows trigger on
protected_branch(check bothpush_branchesandpr_branches; treat*as matching any branch), emit high: "Required check<check>on branch<branch>(ruleset<id>) is not produced by any workflow triggering on that branch. PRs to<branch>will be permanently BLOCKED. Either add the branch to the workflow'son.push.branches/on.pull_request.branches, or remove the check from the ruleset'srequired_status_checks."
Aggregation rule 3 — Pass collation. Each subagent's report ends with a "passes" / "already hardened" section. Concatenate these into ### Already hardened, prefixed with the source subagent: [Actions] persist-credentials: false on all checkouts, [Repo Settings] secret_scanning enabled, [Package Manager] minimumReleaseAge: 4320 with Strict: true.
Aggregation rule 3b — Not-checked collation. Anything that did not run gets a line in ### Not checked, never in ### Already hardened — a skip listed among the passes reads as a pass. One line per item, with its reason: every one of the five areas skipped by Step 1 routing; a gh api 404 or auth failure that dropped repo settings; a subagent that returned nothing or errored; an item a subagent itself skipped and declared in its header (Subagent B's npm-publisher items when release_publishes_to_npm is false, Subagent D's items 2-8 on an unsupported manager); and any tool the detection needed but did not find (gh, jq, yq). Areas with nothing outstanding still appear here as [<area>] Fully checked only if you list them for all five — otherwise list only the gaps.
Aggregation rule 4 — Sequencing hints. If any finding pair has a "fix A before C is actionable" relationship (rule 1's second case is canonical; the rule-2 BLOCKED-PR finding depends on the ruleset existing at all; D's allowBuilds fix may break installs until paired with strictDepBuilds), surface the ordering in a final ### Sequencing section: 1. Apply [Actions] findings → 2. Re-run audit → 3. Apply [Repo Settings] sha_pinning_required recommendation. Render the section only if at least one relationship exists.
Weighting. release.yml is the privileged target — weight findings there above equivalent findings in ordinary CI.
Sort findings within each severity bin worst-first (most exploitable first; policy findings after CVE-rated findings of the same severity). Keep file:line / API-path references intact from each subagent's report.
Close the step with one line: Aggregated 4 auditor reports: 19 raw findings -> 14 after cross-checks (1 critical, 5 high, 6 medium, 2 low), 6 passes, 3 not checked.
Report skeleton — every heading below is rendered, ### Sequencing only when rule 4 produced a relationship:
## Security Audit Report
<one line: areas audited, areas not checked>
### Critical
### High
### Medium
### Low / Informational
### Already hardened
### Not checked
### Sequencing
A filled report:
## Security Audit Report
Audited Actions, Release, Repo Settings, Package Manager. Code not checked (config-only repo).
### Critical
- **Deploy secrets reachable from any branch** — `[Repo Settings]` `CLOUDFLARE_API_TOKEN` is a
repo-level secret and `.github/workflows/deploy.yml:14` references it under a bare `push:`.
Any branch can exfiltrate production credentials. Move it to a branch-restricted `environment:`
(checklist item 10 — create the environment first, delete the repo-level copy last).
### High
- **12 unpinned actions** — `[Actions]` `ci.yml:9,17,31`, `release.yml:12,20` use tags. Run
`pinact run`; a moved tag is arbitrary code holding your token.
- **No tag ruleset on `refs/tags/v*`** — `[Repo Settings]` `release.yml` triggers on `v*` tags and
nothing blocks `update` or `deletion`, so a tag can be re-pointed after review.
`gh api -X POST /repos/<owner>/<repo>/rulesets --input tag-ruleset.json`
- **Release-age gate unset** — `[Package Manager]` (policy) `pnpm-workspace.yaml` has no
`minimumReleaseAge`. Add `minimumReleaseAge: 4320` (minutes), `minimumReleaseAgeStrict: true`.
### Medium
- **`npm publish`, not `npm stage publish`** — `[Release]` `release.yml:41`. Staging is the one
control a fully compromised CI job cannot forge. Requires npm >= 11.15.0.
- **No workflow linter in CI** — `[Actions]` nothing runs zizmor, so the above regresses silently.
### Low / Informational
- **`allowed_actions: "all"`** — `[Repo Settings]` common and appropriate for a public repo.
### Already hardened
- `[Actions]` `persist-credentials: false` on all six checkouts; workflow-level
`permissions: contents: read` on every workflow
- `[Repo Settings]` secret scanning and push protection enabled
- `[Release]` credentialed publish job holds only auth, download, `publish --ignore-scripts`
### Not checked
- `[Code]` Skipped — configuration-only repository, no application source
- `[Repo Settings]` `immutable-releases` — `gh api` returned `404` on this plan; state unknown
- `[Release]` publisher-side npm settings (Trusted Publisher pinning, token-based publishing
disallowed) — not API-auditable; confirm these on npmjs.com yourself
### Sequencing
1. Apply `[Actions]` pin findings -> 2. Re-run this audit -> 3. Enable `sha_pinning_required`.
Step 4: Hand Off Findings
The audit stops here. Step 4 produces two artifacts side by side — a task queue for
downstream work, and a Planned Changes report for the user to read. Do not edit files, run
gh api writes, create issues, or invoke /maintain:repo-hardening,
/build:fix-and-reverify, /plan:issue-flow, or any other skill on the user's behalf.
Hosts differ in whether they have a task tracker; where none exists, the Planned Changes report is the handoff and the options below collapse to option 4.
Ask, per Asking the User:
Emit tasks + planned-changes report(Recommended) — one task per finding, plus the report belowEmit tasks flagged for issue filing— same as 1, withintended_action: file_issuemetadataEmit a single summary task— one task summarising all findings, suitable for a single tracking issueSkip — just the report
All four options render the Planned Changes report (so the user always sees the change plan and revert commands). Options 1–3 additionally emit tasks.
Task emission rules (options 1, 2, 3)
Skip this subsection entirely when the host has no task tracker.
- One task per finding from the Step 3 report (options 1, 2). For option 3, emit a single task whose description is a numbered list of all findings.
subject: short finding title (e.g., "Pinactions/checkout@v6to SHA").description: severity (including any(policy)qualifier), source subagent ([Actions],[Release],[Repo Settings],[Package Manager],[Code]), file:line or API path, the exact remediation snippet from the report.metadata: minimum set —severity,source_area,change_kind(codefor file edits /server_stateforgh apiwrites),intended_action(fix/file_issue/summary). Keep metadata light — the task queue is a handoff, not the audit's persistence layer.- Sequencing: for any pair from Step 3's
### Sequencingsection, record the downstream task as blocked by the upstream one, using whatever dependency field the host's tracker exposes. If it has none, state the ordering in the task description. - Emit tasks worst-first within each severity, matching the report's order.
Planned Changes report
Render directly in the chat output, grouped by source area. It is the audit's plan-of-record,
not an execution transcript — phrasing matters. Use Recommended command: and
Revert command: (never Applied:), because the audit does not observe what actually runs.
Code edits get one line; server-state changes get three.
## Security Audit — Planned Changes
Plan-of-record (not an execution transcript). Render of the Step 3 findings as code edits and `gh api` calls a downstream skill or human will apply.
### [Actions] (N planned changes)
- `.github/workflows/ci.yml` — pin 6 `uses:` lines via `pinact run`
- `.github/workflows/release.yml` — add workflow-level `permissions: contents: read`
### [Release] (N planned changes)
- (or: "No changes planned — audit area skipped per Step 1 routing")
### [Repo Settings] (N planned changes)
- Set `default_workflow_permissions: read` on <owner>/<repo>
- Recommended command: `gh api -X PUT /repos/<owner>/<repo>/actions/permissions/workflow -f default_workflow_permissions=read -F can_approve_pull_request_reviews=false`
- Revert command: `gh api -X PUT /repos/<owner>/<repo>/actions/permissions/workflow -f default_workflow_permissions=write -F can_approve_pull_request_reviews=true`
- Enable immutable releases on <owner>/<repo>
- Recommended command: `gh api -X PUT /repos/<owner>/<repo>/immutable-releases`
- Revert command: `gh api -X DELETE /repos/<owner>/<repo>/immutable-releases`
### [Package Manager] (N planned changes)
- `pnpm-workspace.yaml` — add `minimumReleaseAge: 4320`, `minimumReleaseAgeStrict: true`
### Deferred or skipped (informational)
- [Actions] `pull_request_target` finding in `ci-fork.yml` — flagged as `informational`, no fix planned
- [Repo Settings] `sha_pinning_required` — depends on `[Actions]` pin changes per Step 3 sequencing
Format rules:
- Every area gets a section, even if empty, so the user can see which areas had nothing to apply or were skipped.
- Code edits are one line: file path + short description. No
Recommended command:block — the edit content lives in the task description. - Server-state changes are three lines: title,
Recommended command:,Revert command:. Both are the literalgh apiinvocations from the Step 3 finding. - A
### Deferred or skippedsection captures anything not fixed (informational items, sequencing-blocked items). Nothing is silently dropped.
Handoff line
After rendering the report and emitting tasks, print one of:
- Option 1: "N tasks emitted. Run
/maintain:repo-hardeningfor thegh apichanges, or/build:fix-and-reverifyto walk through the file edits." - Option 2: "N tasks emitted with
file_issueflag. Run/plan:issue-flowper task when you're ready." - Option 3: "Single summary task emitted. Run
/plan:issue-flowto file it as a tracking issue." - Option 4: "No tasks emitted; the report above is the full audit output."
Then stop. Do not invoke any of those skills yourself.
References
| File | Contents |
|---|---|
references/subagent-prompts.md |
Dispatch prompt per audit area, plus the placeholder substitution table |
references/severity-table.md |
Severity per finding shape; the release-age gate's four units |
references/actions-checklist.md |
Detection, severity, and fix for each workflow check |
references/release-checklist.md |
Release trigger, provenance, staged publishing, artifact identity |
references/repo-settings-checklist.md |
Exact gh api detection and remediation commands, bypass-actor patterns |
references/package-manager-checklist.md |
Per-manager field names, units, defaults, and fix examples |
references/install-flags.md |
Frozen-lockfile and --prod flags per manager, multi-stage Docker |
references/code-security-checklist.md |
Injection and unsafe-execution vectors by stack |