Skip to content
Skip to content
Documentation

zizmor (GitHub Actions)

zizmor is a static analyzer for GitHub Actions. ASH runs it over a repository's workflows and composite actions and reports template injection, dangerous triggers, credential persistence, unpinned actions, excessive permissions and the rest of zizmor's audits.

actionlint, also a builtin scanner, flags template injection from untrusted event data too. ASH does not deduplicate across scanners, so with both enabled such a step is reported by each, under its own rule id.

Enabling it

zizmor is a builtin scanner, enabled by default: a default scan runs it. scanners.zizmor.enabled: false in the ASH config turns it off, and --scanners and --exclude-scanners select it as they select any scanner. If no zizmor satisfying tool_version is found, ASH installs one with uv tool install (online only). If that fails, the scanner is reported MISSING and the scan exits 1 (the incomplete-scan gate), as for every builtin scanner; ash dependencies install and the container image provide it.

What gets scanned

ASH passes zizmor two kinds of file, taken from its scan set so .gitignore, .ignore and global_settings.ignore_paths apply (the ignore files only while honor_ignore_files is on, the default):

  • workflows: *.yml and *.yaml directly inside a .github/workflows directory (the only place GitHub runs a workflow from; subdirectories are not scanned), including when the source directory is itself .github or .github/workflows;
  • composite actions: every action.yml and action.yaml.

Files under directories named node_modules, .venv or venv are skipped, and so is a file that is a symlink resolving outside the scan root, with a warning: zizmor would read it where it points. In the source directory, such a link is left out by the scan set before zizmor's check sees it, with the warning Not scanning '<path>': link leaves the scanned tree, and is listed among the inputs not scanned (see What decides the files). A broken symlink (its target is missing, or links loop) is skipped with a warning too, since zizmor would fail the whole run on it. A repository with neither kind of file does not start zizmor; the scanner reports SKIPPED with zero findings, the same as cfn-nag on a repository with no templates.

zizmor is a Python package ASH runs through uv, so --python-only runs it.

A file named action.yml that is not a GitHub Actions definition is not counted. A workflow or action that zizmor cannot load (a YAML syntax error, or a file in .github/workflows that is not a valid workflow) is counted as a target that failed, which --fail-on-incomplete-scanners reports.

Configuration

scanners:
  zizmor:
    enabled: true
    options:
      persona: regular        # regular, pedantic or auditor
      config_file: null       # zizmor config; relative to where it is set, or absolute
      online_audits: false    # see "Network access and tokens"
      honor_inline_ignores: false  # see "Suppressions"
      tool_version: ">=1.29.0,<2.0.0"
      install_timeout: 300
      scan_timeout: 1800
      severity_threshold: null
  • persona: zizmor's persona. regular has the fewest false positives, pedantic adds code-smell findings, and auditor reports everything.
  • config_file: passed as zizmor --config, when the operator set it: from --config-overrides, or an ASH config file outside every git checkout and the scanned directory (one that is not repository-written). A relative path is taken from the directory of the ASH config file that sets it, or from the working directory for --config-overrides, not from the source directory (see Relative paths in the operator's settings); an absolute path is used as given. Without it, ASH runs zizmor --no-config, so a zizmor.yml or .github/zizmor.yml in the scanned repository is not read: a config there can disable audits or ignore findings, which zizmor then never reports, so they would be neither in ASH's results nor counted as suppressed. config_file set by an ASH config inside a git checkout or the scanned directory is ignored with a warning, and an MCP client cannot set it at all (see online_audits below). A configured file that does not exist fails the scan rather than being ignored. The file may be inside the scanned tree, but the scanned repository decides what is at such a path, so one there that leads out of the tree through a symbolic link fails the scan with ERROR and zizmor is not run. zizmor quotes the offending text of a config it cannot parse in its error, and that error reaches ASH's log and the scanner's stderr log, so do not point this at a file holding anything else.
  • honor_inline_ignores: when true, zizmor applies its own # zizmor: ignore[...] comments and the ignore rules of an operator's config_file. Off by default; see "Suppressions". Honored only when the operator sets it.
  • tool_version: the pip-style constraint for installing zizmor. A value that is not a valid constraint is ignored with a warning and the default is used. It decides which zizmor is installed and which one on PATH is accepted, so a value other than the default is honored only when the operator sets it (--config-overrides, or an ASH config file outside every git checkout and the scanned directory); set by an ASH config inside one, it is ignored with a warning.
  • install_timeout: seconds allowed for uv tool install of zizmor.
  • scan_timeout: seconds zizmor may run; past it zizmor is killed and the scanner reports ERROR.
  • severity_threshold: this scanner's override of the global severity threshold.

Network access and tokens

ASH always runs zizmor --offline unless online_audits: true. Offline is the default because zizmor reads a GitHub token from GH_TOKEN, GITHUB_TOKEN or ZIZMOR_GITHUB_TOKEN by itself, and a token that happens to be in a CI job's environment is not permission to use it. While offline, ASH removes those three variables from zizmor's environment.

online_audits: true drops zizmor --offline and lets those variables reach zizmor unchanged, which enables the audits that query the GitHub API (for example known-vulnerable actions and impostor commits). It is honored only when the operator sets it, with --config-overrides or an ASH config file outside every git checkout and the scanned directory; set by a config inside one, it is ignored with a warning and zizmor runs offline without the tokens. An MCP client cannot configure zizmor at all: its whole section, /scanners/zizmor/**, is denied to a client whatever a profile's denied_paths says, as for every scanner that can send data off the host. ASH never reads, copies or logs the token and never puts it on zizmor's command line. ASH's own offline mode (ASH_OFFLINE=true or ash scan --offline) overrides online_audits. Under --sandbox, zizmor never gets the token: the sandbox passes no GH_TOKEN, GITHUB_TOKEN or ZIZMOR_GITHUB_TOKEN, so online audits cannot authenticate. It gets a network only when the operator lists zizmor in sandbox.network_scanners, and never when the scan is offline.

ZIZMOR_CONFIG, ZIZMOR_OFFLINE and ZIZMOR_NO_ONLINE_AUDITS are always removed from zizmor's environment, so a scan's result depends on the repository and the ASH config rather than on the shell it ran in. Use config_file instead of ZIZMOR_CONFIG.

Severity mapping

zizmor rates each finding with a severity and a confidence. ASH combines them:

zizmor severity confidence Medium or High confidence Low
High HIGH MEDIUM
Medium MEDIUM LOW
Low LOW INFO
Informational INFO INFO

zizmor's scale stops at High, so no zizmor finding is CRITICAL. A low-confidence finding is one zizmor expects to be wrong some of the time, so it is reported one band lower. The SARIF level is set to match (HIGH error, MEDIUM warning, LOW note, INFO none), and zizmor's own values stay on the result as properties["zizmor/severity"] and properties["zizmor/confidence"]. A finding missing either value keeps zizmor's own level.

With the default MEDIUM threshold, the low-confidence artipacked finding (Medium, Low) is reported LOW and does not fail the scan on its own.

Suppressions

Rule IDs are zizmor's audit names with a zizmor/ prefix, for example zizmor/template-injection. ASH suppressions by rule, path and line all apply:

global_settings:
  suppressions:
    - rule_id: zizmor/dangerous-triggers
      path: .github/workflows/release.yml
      line_start: 3
      reason: "Runs only on tags pushed by maintainers"

zizmor's own ignore comments (# zizmor: ignore[template-injection]) are not honored by default. Such a comment makes zizmor drop the finding before ASH sees it, so it would be neither reported nor counted as suppressed; ASH runs zizmor --no-ignores, which also turns off the ignore rules of an operator's config_file (its other settings still apply). To honor both, the operator sets honor_inline_ignores: true with --config-overrides or in an ASH config file outside every git checkout and the scanned directory; set by an ASH config inside one, it is ignored with a warning. A zizmor.yml in the scanned repository is never read. Package-scoped and symbol-scoped suppressions do not apply: zizmor reports no packages, and a workflow has no functions or classes.

Installation

ash dependencies install installs zizmor with uv tool install, the newest release within tool_version, and the ASH container image ships zizmor 1.30.1. ASH also uses any zizmor already on PATH whose zizmor --version satisfies tool_version, which covers the nix flake (nixpkgs supplies zizmor), Homebrew, and cargo install. Offline, ASH does not try to install zizmor; install it first.

zizmor is MIT licensed.