Skip to content
Fran Gonzalez
← Back to blog
(updated Jul 16, 2026)·Clanker·7 min read

Fixing zizmor findings without suppressions

Three fix patterns for common zizmor findings: creating environments via gh api, hardening git auth with remote set-url, and replacing third-party actions with native commands.

Some matmuls wrote this slop, sorry. My goal with this content is to document some work I (a real human bean) do while poking the Clanker, and try to learn something along the way.

I wrote earlier about discovering six findings that --pedantic missed. The --persona=auditor --no-ignores flag surfaced them. This post covers what I did with them.

Findings to resolve

Suppressions rot. A zizmor.yml entry or an inline # zizmor: ignore[rule] comment takes seconds; a real fix takes understanding the rule and finding the right approach. I covered the same pattern with pnpm exclusions in keeping supply chain exclusions honest: trustPolicyExclude entries age out as upstream packages adopt provenance. minimumReleaseAgeExclude entries stay long after the package passes the age window. zizmor suppressions decay the same way. The override outlives the reason for it.

When --persona=auditor --no-ignores surfaced six findings across two repos, my first instinct for one of them was a suppression comment. It took five minutes to realize every finding had a real fix that was simpler than maintaining a suppression over time.

Remediation patterns

Creating an environment via gh api

The secrets-outside-env rule flags secrets accessed from jobs that lack an explicit environment: declaration1. Without one, a secret is available to any job in the workflow, including jobs triggered by pull_request from forked repos. An attacker who submits a PR that modifies .github/workflows/ci.yml gains access to every secret the workflow references.

GitHub deployment environments provide scoping plus three optional protection rules:

MechanismWhat it does
Environment scopingSecrets are only available to jobs that declare environment: name
Required reviewersA human must approve before the job can access the environment
Wait timerA minimum delay before the environment is available
Branch restrictionsOnly runs from specific branches can access the environment

Environments without any protection rules are purely declarative scoping: the secret is gated behind the environment name, and the environment gate has no approval friction.

A DAGGER_CLOUD_TOKEN was used in a CI job without an environment. Dagger Cloud’s token is a monitoring credential with zero infrastructure access, so protection rules would add CI friction for no security gain. The fix is adding environment: dagger and creating an empty environment. gh has no environment subcommand, but the REST API does:

gh api repos/owner/repo/environments/dagger --method PUT -f wait_timer=0

This creates an environment named dagger with zero protection rules. The workflow change is one line:

# .github/workflows/ci.yml
jobs:
  ci:
    name: Run CI checks
    runs-on: ubuntu-latest
    environment: dagger
    steps:
      - uses: dagger/dagger-for-github@v8
        with:
          cloud-token: ${{ secrets.DAGGER_CLOUD_TOKEN }}

The token stays at the repo level; the environment only scopes where it is referenced. For deploy secrets (PRODUCTION_TOKEN, NPM_TOKEN), add required reviewers and branch restrictions to the same environment: block. For CI monitoring tokens, an empty environment is enough.

My first instinct was an inline # zizmor: ignore[secrets-outside-env] comment. The real fix was a single API call and one YAML line. The suppression would have outlived both the tool version and my memory of why it was there.

Hardening git auth without persisting credentials

The artipacked rule flags persist-credentials: true on actions/checkout2. By default, the action writes the GITHUB_TOKEN to the local .git/config, where it can be captured by subsequent steps and uploaded as build artifacts. Three dispatch workflows had it because they push generated content files to branches, which needs git authentication. Setting it to false breaks the push.

The fix keeps persist-credentials: false on checkout and injects the token into the origin URL at runtime, right before the push:

# Before
- uses: actions/checkout@v6
  with:
    persist-credentials: true

- name: Create pull request
  uses: peter-evans/create-pull-request@v7
  with:
    token: ${{ secrets.GITHUB_TOKEN }}

# After
- uses: actions/checkout@v6
  with:
    persist-credentials: false

- name: Configure git for push
  env:
    GH_TOKEN: ${{ github.token }}
    REPO: ${{ github.repository }}
  run: |
    git remote set-url origin "https://git:${GH_TOKEN}@github.com/${REPO}.git"

# Subsequent git push commands work normally

The token lives in the git remote URL for the duration of the step. When the step ends, the environment variable is gone and the remote URL resets on the next checkout. Same git push capability, no persisted credential in the git config.

Native git + gh pr create instead of a third-party action

Two dispatch workflows used peter-evans/create-pull-request to commit generated files and open a PR. The superfluous-actions rule flags it because the runner already provides equivalent functionality through gh pr create and native git commands.

Replacing it required replicating five behaviors: set git identity, create a branch, commit files, push the branch, open a PR with labels and a body. None of this needs a third-party action:

# Before
- uses: peter-evans/create-pull-request@v7
  with:
    token: ${{ secrets.GITHUB_TOKEN }}
    commit-message: "article: add ${{ github.event.client_payload.slug }}"
    branch: "article/${{ github.event.client_payload.slug }}"
    delete-branch: true
    labels: |
      content
      auto-merge
    body: |
      ## New article from dispatch
      ...

# After
- name: Create pull request
  env:
    GH_TOKEN: ${{ github.token }}
    BRANCH: article/${{ github.event.client_payload.slug }}
  run: |
    git config user.name "github-actions[bot]"
    git config user.email "github-actions[bot]@users.noreply.github.com"
    git checkout -b "$BRANCH"
    git add .
    git commit -m "article: add ${SLUG}"
    git push origin "$BRANCH"
    gh pr create \
      --repo "$REPO" \
      --base main --head "$BRANCH" \
      --title "article: add ${SLUG}" \
      --label "content" --label "auto-merge" \
      --body-file /tmp/pr-body.md

The --body-file flag avoids shell quoting issues with multiline PR bodies. A heredoc writes the body to a temp file, gh pr create reads it. Branch cleanup moved to the existing gh pr merge --auto --squash --delete-branch step.

Follow-up

I would run --persona=auditor --no-ignores before adding any suppression. For the secrets-outside-env finding, I had the inline comment typed out before I stopped and checked whether a fix existed. The API call was shorter than the comment.

All three patterns apply to any repo. The gh api call for environments works anywhere gh is authenticated. The git remote set-url pattern works in any workflow that needs git push. Replacing peter-evans/create-pull-request with native commands removes a dependency from the supply chain. Each fix took less time than writing a justified suppression comment that would need maintaining.

References

Footnotes

  1. Without an environment: declaration, secrets are available to any job in the workflow. This includes pull_request runs from forked repos, where an attacker who modifies the workflow gains access to every secret it references. GitHub’s environment documentation describes required reviewers, wait timers, and branch restrictions as optional controls layered on top of environment scoping. An environment with zero protection rules provides pure scoping: the secret is gated behind the environment name, and the gate has no approval friction.

  2. The ArtiPACKED attack exploits the default persist-credentials: true in actions/checkout. The token written to .git/config can be captured by any subsequent step and uploaded as a build artifact. StepSecurity’s ArtiPACKED detection and Palo Alto Networks’ analysis of token leakage through artifacts document the attack in detail. The actions/checkout maintainers have an open issue proposing to change the default to false.

This post was written with AI assistance.