---
description: Usage tips and recipes for running zizmor locally and in CI/CD.
---

# Usage

## Input collection

Before auditing, `zizmor` performs an input collection phase.

There are four input sources that `zizmor` knows about:

1. Individual input files, e.g. `foo.yml`, `my-action/action.yml`,
   and `.pre-commit-config.yaml`;
2. "Local" repositories in the form of a directory, e.g. `my-repo/`;
3. "Remote" (GitHub) repositories in the form of a "slug", e.g.
   `pypa/sampleproject`;

    !!! tip

        By default, a remote repository will be audited from the `HEAD`
        of the default branch. To control this, you can append a `git`
        reference to the slug:

        ```bash
        # audit at HEAD on the default branch
        zizmor example/example

        # audit at branch or tag `v1`
        zizmor example/example@v1

        # audit at a specific SHA
        zizmor example/example@abababab...
        ```

    !!! tip

        Remote auditing requires Internet access and a GitHub API token.
        See [Operating Modes](#operating-modes) and
        [GitHub API token permissions](#github-api-token-permissions) for more
        information.

4. Standard input, via `-`.

    !!! tip

        Support for auditing from standard input is available in `v1.24.0`
        and later.

    ```bash
    # pipe a workflow from stdin
    cat workflow.yml | zizmor -

    # or use a heredoc
    zizmor - <<'EOF'
    on: push
    jobs:
      test:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v3
    EOF
    ```

    When reading from stdin, `zizmor` automatically infers the input type
    (workflow, action, Dependabot config, or pre-commit config/hook definition).

    !!! note

        `-` cannot be combined with other inputs, and `--fix` is not
        supported with stdin input.

`zizmor` can audit multiple inputs in the same run, and different input
sources can be mixed and matched:

```bash
# audit a single local workflow, an entire local directory, and
# a remote repository all in the same run
zizmor ../example.yml ../other-repo/ example/example
```

When auditing local directories and/or remote repositories, `zizmor` will
collect all known input kinds by default. To configure collection behavior,
you can use the `--collect=...` option.

```bash
# collect everything regardless of `.gitignore` patterns
zizmor --collect=all example/example

# collect everything while respecting `.gitignore` patterns (the default)
zizmor --collect=default example/example

# collect only workflows
zizmor --collect=workflows example/example

# collect only actions
zizmor --collect=actions example/example

# collect only Dependabot configs
zizmor --collect=dependabot example/example

# collect only pre-commit inputs
zizmor --collect=pre-commit example/example

# collect both workflows and actions, but nothing else
zizmor --collect=workflows,actions example/example
```

!!! tip

    `--collect=all` can be significantly slower than `--collect=default`,
    particularly when collecting from directories that contain large
    hierarchies of paths that would be ignored by `.gitignore` patterns.

!!! tip

    `--collect=...` only controls input collection from repository input
    sources. In other words, `zizmor --collect=actions workflow.yml`
    *will* audit `workflow.yml`, since it was passed explicitly and not
    collected indirectly.

By default, `zizmor` will warn (but not fail) if it fails to parse an
input file. To turn these warnings into failures,
you can use the `--strict-collection` option:

```bash
# fail if any files collected from example/example fail to parse
zizmor --strict-collection example/example
```

!!! important

    `--strict-collection` is available in `v1.7.0` and later.

## Operating Modes

`zizmor` has three *operating modes*:

* **Offline**: `zizmor` will not access the Internet,
  will not fetch remote repositories, and will not perform
  any audits that require online access (e.g. to the GitHub API).
* **Online**: `zizmor` will fetch remote repositories
  as necessary, and will perform all audits that require
  online access.
* **Online sans audits**: `zizmor` will fetch remote repositories
  as necessary, but will skip any audits that require online access.

`zizmor` attempts to select a default operating mode based on the user's
environment:

- If `ZIZMOR_OFFLINE` is set, then `zizmor` runs in offline mode.
- If `GH_TOKEN`, `GITHUB_TOKEN` or `ZIZMOR_GITHUB_TOKEN` is set, then `zizmor` runs in online mode with audits.
    - Additionally `ZIZMOR_NO_ONLINE_AUDITS` is set, then `zizmor` runs
      online sans audits.
- If neither `ZIZMOR_OFFLINE` nor `GH_TOKEN`/`GITHUB_TOKEN`/`ZIZMOR_GITHUB_TOKEN` are set, then `zizmor` runs
  in offline mode.

Or, as a flowchart:

```mermaid
flowchart TD
  B{ZIZMOR_OFFLINE set?} -- Yes --> C[Offline mode]
  B -- No --> D{GH_TOKEN/GITHUB_TOKEN/ZIZMOR_GITHUB_TOKEN set?}
  D -- No --> C
  D -- Yes --> E{ZIZMOR_NO_ONLINE_AUDITS set?}
  E -- Yes --> F[Online sans audits mode]
  E -- No --> G[Online mode]
```

Each operating mode can also be made explicit through command-line flags:

```bash
# force offline, even if GH_TOKEN/GITHUB_TOKEN/ZIZMOR_GITHUB_TOKEN is present
# this disables all online actions, including repository fetches
zizmor --offline workflow.yml

# passing a token explicitly will enable online mode
# in this case, we use the `gh` CLI token
zizmor --gh-token $(gh auth token) workflow.yml

# online for the purpose of fetching the input (example/example),
# but all audits themselves are offline
zizmor --no-online-audits --gh-token $(gh auth token) example/example
```

### GitHub API token permissions

!!! tip

    If you're running `zizmor` locally with an online mode, it is
    **strongly recommended** that you use `gh auth token` from the [GitHub CLI]
    to automatically access an API token and avoid error-prone manual
    provisioning.

    This token is almost always pre-configured with sufficient permissions,
    meaning that you **do not** need any of the manual steps below.

    [GitHub CLI]: https://cli.github.com/

When running in an online mode, `zizmor` uses the GitHub API for
various operations, including fetching a repository's contents
(for the collection phase) and fetching tag/branch/commit histories
(for the auditing phase).

Generally speaking, `zizmor` does **not** require any special permissions
from its GitHub API token: it uses the token *primarily* to provide
authentication for rate-limited API requests.

However, if you are auditing a private remote repository (e.g.
`zizmor myorg/private-repo`) or auditing _any_ input that contains
private references (e.g. `#!yaml uses: myorg/private-action`), then
you may need to provision permissions for the token:

- `#!yaml contents: read` for GitHub Actions
- `repo` for OAuth tokens and "classic" PATs
- An appropriate repository access scope for fine-grained PATs

In addition to these permissions, some integrations of `zizmor`
(like GitHub Advanced Security) may require additional permissions
in the context of GitHub Actions. See
[Integration - GitHub Actions](./integrations.md#github-actions) for more details.

## Output formats

`zizmor` always produces output on `stdout`.

### Cargo-style output ("plain")

By default, `zizmor` produces `cargo`-style diagnostic output.

```console
error[template-injection]: code injection via template expansion
  --> ./tests/integration/test-data/template-injection/pr-425-backstop/action.yml:31:56
   |
29 |       uses: azure/powershell@whatever
   |       ------------------------------- action accepts arbitrary code
30 |       with:
31 |         inlineScript: Get-AzVM -ResourceGroupName "${{ inputs.expandme }}"
   |         ------------ via this input                    ^^^^^^^^^^^^^^^ may expand into attacker-controllable code
   |
   = note: audit confidence → High
```

This output will be colorized by default when sent to a supporting terminal and
uncolorized by default when piped to another program. Users can also explicitly
disable output colorization by setting `NO_COLOR=1` in their environment.

This format can also be explicitly selected with `--format=plain`.

#### Audit documentation links

By default, `zizmor` includes links to relevant documentation pages
for each finding in its plain output format. These links are provided via
[OSC 8](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda)
hyperlinks, which are supported by many modern terminal emulators.

For example, in the output above, `template-injection` within
`error[template-injection]: ...` is a clickable link that takes you to
the [template-injection](./audits#template-injection) audit documentation.

In addition to these OSC 8 links, `zizmor` also includes the full URL
as part of each finding _if_ it detects a non-terminal output _or_
a CI environment (e.g. GitHub Actions).

To make this behavior explicir, users can supply the `--show-audit-urls`
option:

```bash
# always show audit documentation URLs, even if output is to a terminal
zizmor --show-audit-urls=always ...

# never show audit documentation URLs
zizmor --show-audit-urls=never ...
```

!!! note

    `--show-audit-urls=...` is available in `v1.19.0` and later.

#### Color customization

When invoked from a terminal, `zizmor` will attempt to enrich its output
with ANSI colors.

!!! note

    `--color` is available in `v1.5.0` and later.

Some users may prefer to explicitly enable or disable this behavior. For
example, GitHub Actions is not a terminal but it does support ANSI colors,
so enabling colors in GitHub Actions can make logs more readable.

To explicitly control `zizmor`'s colorization behavior, use the
`--color` option:

```bash
# force colorization
zizmor --color=always ...

# force no colorization
zizmor --color=never ...
```

`zizmor` also respects various environment variables for colorization:

* [`NO_COLOR`](https://no-color.org/): if set to any value, disables colorization
* [`FORCE_COLOR`](https://force-color.org/): if set to any value, enables colorization
* [`CLICOLOR_FORCE`](https://bixense.com/clicolors/): if set to any value, enables colorization

### JSON

!!! important

    The JSON format is versioned, and `--format=json` is an alias for the
    current version.

    The current version of the JSON format is `v1`. You can use
    `--format=json-v1` to explicitly select the current version.

    The following compatibility policy is used for JSON format versions:

    1. The current version of the format is always aliased as `json`.
    2. When a new version of the JSON format is added, `--format=json`
       will show a deprecation warning, and will transition to the new version
       with the next major release.
    3. When a new version of the JSON format is added, explicit uses of the
       old version will show a deprecation warning.
    4. The old version will be removed in the *subsequent* major release
       (i.e., the major release after the one that transitions the `json` alias).

!!! important

    `--format=json-v1` is available in `v1.6.0` and later.

!!! important

    `--format=json-v1` uses 0-based line numbering, where line numbers are
    denoted by the key `row`. This is as opposed to `--format=plain` and `--format=SARIF`,
    where line numbers are 1-based.

With `--format=json`, `zizmor` will produce a flat array of findings in
JSON format:

```bash
zizmor --format=json . | jq .[0]
```

??? Example "Example output"

    ```json
    {
      "ident": "template-injection",
      "desc": "code injection via template expansion",
      "url": "https://docs.zizmor.sh/audits/#template-injection",
      "determinations": {
        "confidence": "High",
        "severity": "High",
        "persona": "Regular"
      },
      "locations": [
        {
          "symbolic": {
            "key": {
              "Local": {
                "verbatim_path": "./.github/workflows/ci.yml"
              }
            },
            "annotation": "this step",
            "route": {
              "route": [
                {
                  "Key": "jobs"
                },
                {
                  "Key": "greet"
                },
                {
                  "Key": "steps"
                },
                {
                  "Index": 0
                }
              ]
            },
            "feature_kind": "Normal",
            "kind": "Hidden"
          },
          "concrete": {
            "location": {
              "start_point": {
                "row": 6,
                "column": 8
              },
              "end_point": {
                "row": 7,
                "column": 0
              },
              "offset_span": {
                "start": 86,
                "end": 136
              }
            },
            "feature": "run: echo \"Hello ${{ github.event.issue.title }}\"\n",
            "comments": []
          }
        },
        {
          "symbolic": {
            "key": {
              "Local": {
                "verbatim_path": "./.github/workflows/ci.yml"
              }
            },
            "annotation": "may expand into attacker-controllable code",
            "route": {
              "route": [
                {
                  "Key": "jobs"
                },
                {
                  "Key": "greet"
                },
                {
                  "Key": "steps"
                },
                {
                  "Index": 0
                },
                {
                  "Key": "run"
                }
              ]
            },
            "feature_kind": {
              "Subfeature": {
                "after": 13,
                "fragment": {
                  "Raw": "github.event.issue.title"
                }
              }
            },
            "kind": "Primary"
          },
          "concrete": {
            "location": {
              "start_point": {
                "row": 6,
                "column": 29
              },
              "end_point": {
                "row": 6,
                "column": 53
              },
              "offset_span": {
                "start": 107,
                "end": 131
              }
            },
            "feature": "echo \"Hello ${{ github.event.issue.title }}\"",
            "comments": []
          }
        },
        {
          "symbolic": {
            "key": {
              "Local": {
                "verbatim_path": "./.github/workflows/ci.yml"
              }
            },
            "annotation": "this run block",
            "route": {
              "route": [
                {
                  "Key": "jobs"
                },
                {
                  "Key": "greet"
                },
                {
                  "Key": "steps"
                },
                {
                  "Index": 0
                },
                {
                  "Key": "run"
                }
              ]
            },
            "feature_kind": "KeyOnly",
            "kind": "Related"
          },
          "concrete": {
            "location": {
              "start_point": {
                "row": 6,
                "column": 8
              },
              "end_point": {
                "row": 6,
                "column": 11
              },
              "offset_span": {
                "start": 86,
                "end": 89
              }
            },
            "feature": "run",
            "comments": []
          }
        }
      ],
      "ignored": false,
      "fixes": [
        {
          "title": "replace expression with environment variable",
          "key": {
            "Local": {
              "verbatim_path": "./.github/workflows/ci.yml"
            }
          },
          "disposition": "unsafe"
        }
      ]
    }
    ```

### SARIF

`zizmor` supports SARIF via `--format=sarif`.
SARIF is a JSON-based standard for representing static analysis results.

See [Integration - GitHub Actions](./integrations.md#github-actions) for
information on using `zizmor` with GitHub's Advanced Security
functionality via GitHub Actions.

### GitHub Annotations

!!! note

    `--format=github` is available in `v1.6.0` and later.

`zizmor` supports GitHub annotations via `--format=github`.

See [Workflow Commands for GitHub Actions] for additional information about
annotations.

[Workflow Commands for GitHub Actions]: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands

!!! warning

    GitHub annotations come with significant limitations: a single CI step
    can only render 10 annotations at a time.

    If your `zizmor` run produces more than 10 findings, only the first 10 will
    be rendered; all subsequent findings will be logged in the actions log but
    **will not be rendered** as annotations.

    See orgs/community?26680 and orgs/community?68471 for additional
    information.


## Exit codes

`zizmor` will exit with a variety of exit codes, depending on how
you invoke it and what happens during the run. In general:

| Code | Meaning |
| ---- | ------- |
| 0    | Successful audit; no findings to report (or SARIF mode enabled). |
| 1    | Error during audit; consult output. |
| 2    | Argument parsing failure; consult output. |
| 3    | No inputs were collected. |
| 11   | One or more findings found; highest finding is "informational" level. |
| 12   | One or more findings found; highest finding is "low" level. |
| 13   | One or more findings found; highest finding is "medium" level. |
| 14   | One or more findings found; highest finding is "high" level. |

All other exit codes are currently reserved.

!!! note

    Exit code `3` is available in `v1.21.0` and later. Versions before this
    do not assign any special meaning to exit code `3`.

!!! warning "Removal"

    Versions of zizmor prior to `v1.14.0` used exit code `10` to indicate
    the highest finding having "unknown" severity. This exit code is
    no longer used as of `v1.14.0`.

Beyond this general table, the following modes of operation also
change the exit code behavior:

* If you run with `--no-exit-codes`, `zizmor` will **not** use exit codes 11
  and above.
* If you use `--format=sarif`, `zizmor` will **not** use exit codes 11 and
  above.

    !!! tip "Why?"

        Because SARIF consumers generally don't expect SARIF producers
        (like `zizmor`) to exit with a non-zero code *except* in the case
        of an internal failure. SARIF consumers expect semantic results
        to be communicated within the SARIF itself.

* If you run with `--fix` *and* all findings are successfully auto-fixed,
  `zizmor` will **not** use exit codes 11 and above.

    !!! tip "Why?"

        Because the higher exit codes indicate that action needs to be taken,
        but a successful application of all fixes means that no action
        is required.

## Using personas

!!! tip

    `--persona=...` is available in `v0.7.0` and later.

`zizmor` comes with three pre-defined "personas," which dictate how
sensitive `zizmor`'s analyses are:

* The _regular persona_: the user wants high-signal, low-noise, actionable
  security findings. This persona is best for ordinary local use as well as use
  in most CI/CD setups, which is why it's the default.

    !!! note

        This persona can be made explicit with `--persona=regular`,
        although this is not required.


* The _pedantic persona_, enabled with `--persona=pedantic`: the user wants
  *code smells* in addition to regular, actionable security findings.

    This persona is ideal for finding things that are a good idea
    to clean up or resolve, but are likely not immediately actionable
    security findings (or are actionable, but suggest a intentional
    security decision by the workflow/action author).

    For example, using the pedantic persona will flag the following
    with a `template-injection` finding, since it uses a template
    expansion in a `#!yaml run:` block, even though the expansion
    itself is not attacker-controllable:

    ```yaml
    run: |
      echo "running with ${{ github.event_name }}"
    ```

    produces:

    ```console
    $ zizmor --pedantic example.yml
     help[template-injection]: code injection via template expansion
       --> crates/zizmor/tests/integration/test-data/neutral.yml:22:34
       |
    21 |       - run: |
       |         --- this run block
    22 |           echo "running with ${{ github.event_name }}"
       |                                  ^^^^^^^^^^^^^^^^^ may expand into attacker-controllable code
       |
       = note: audit confidence → High
    ```

    !!! tip

        This persona can also be enabled with `--pedantic`, which is
        an alias for `--persona=pedantic`.

* The _auditor persona_, enabled with `--persona=auditor`: the user wants
  *everything* flagged by `zizmor`, including findings that are likely
  to be false positives.

    This persona is ideal for security auditors and code reviewers, who
    want to go through `zizmor`'s findings manually with a fine-toothed comb.

!!! tip

    Some audits, notably [`self-hosted-runner`](./audits.md#self-hosted-runner),
    *only* produce auditor-level results. This is because these audits require
    runtime context that `zizmor` lacks access to by design, meaning that their
    results are always subject to false positives.

    For example, with the default persona:

    ```console
    $ zizmor self-hosted.yml
    🌈 completed self-hosted.yml
    No findings to report. Good job! (1 suppressed)
    ```

    and with `--persona=auditor`:

    ```console
    $ zizmor --persona=auditor self-hosted.yml
    warning[self-hosted-runner]: runs on a self-hosted runner
      --> self-hosted.yml:13:5
       |
    13 |     runs-on: [self-hosted, my-ubuntu-box]
       |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ self-hosted runner used here
       |
       = note: audit confidence → High

    1 finding: 0 informational, 0 low, 1 medium, 0 high
    ```

## Auto-fixing results { #auto-fixing-results }

!!! tip

    `--fix=[MODE]` is available in `v1.10.0` and later, and is
    considered stable as of `v1.15.0`.

Starting with `v1.10.0`, `zizmor` can automatically fix a subset of its findings.

Auto-fixable findings are marked with an additional `note:` annotation
beneath their body, e.g.:

```console hl_lines="10"
error[template-injection]: code injection via template expansion
  --> example.yml:18:36
   |
17 |       - run: |
   |         ^^^ this run block
18 |           echo "doing a thing: ${{ inputs.test }}"
   |                                    ^^^^^^^^^^^ may expand into attacker-controllable code
   |
   = note: audit confidence → High
   = note: this finding has an auto-fix
```

To attempt auto-fixes for *safe* fixes, you can use the `--fix` or
`--fix=safe` option:

```bash
# these two are equivalent
zizmor --fix example.yml
zizmor --fix=safe example.yml
```

### Unsafe fixes

!!! important

    Unsafe fixes **must** be manually reviewed for semantic correctness.

By default, `--fix` will only apply *safe* fixes, i.e. fixes that are
safe to apply with minimal human oversight due to their low breakage risk.

Not all changes are safe, however, and `zizmor` offers *unsafe* fixes
for some findings as well. These fixes are *often* correct, but require
human review.

To apply *unsafe* fixes, you can either use `--fix=all` (to enable both
safe and unsafe fixes) or `--fix=unsafe-only` (to enable only unsafe fixes):

```bash
zizmor --fix=all example.yml
zizmor --fix=unsafe-only example.yml
```

### Limitations

`zizmor`'s auto-fix mode has several limitations that are important
to keep in mind:

* **In-place modification**: `--fix=[MODE]` modifies fixable inputs
  in-place, meaning that the original files will be modified.
* **No remote fixes**: as a corollary to the above, `--fix=[MODE]`
  does not support remote inputs (e.g. `zizmor example/example`).
* **Format preservation**: `--fix=[MODE]` attempts to preserve
  the original format of the input files, including exact indentation
  and comments. However, this is ultimately a heuristic, and
  some patches may not match the file's exact style.
* **Online access**: some fixes may require online access, even if their
  parent audit doesn't. For example, [unpinned-uses](./audits.md#unpinned-uses)
  doesn't require online access to _detect_ unpinned uses, but it does
  require online access (and a GitHub token) to fetch the latest commit
  SHA for pinning.

## Filtering results

There are two straightforward ways to filter `zizmor`'s results:

1. If all you need is severity or confidence filtering (e.g. "I want only
   medium-severity and/or medium-confidence and above results"), then you can use
   the `--min-severity` and `--min-confidence` flags:

    !!! tip

        `--min-severity` and `--min-confidence` are available in `v0.6.0` and later.

    !!! warning "Deprecation"

        `--min-severity=unknown` and `--min-confidence=unknown` are
        **deprecated** as of `v1.14.0` and will be removed in a future release.
        Users should omit these entirely, as they were no-ops even prior to
        deprecation.

     ```bash
     # filter informational, and low findings with low confidence
     zizmor --min-severity=medium --min-confidence=medium ...
     ```

2. If you need more advanced filtering (with nontrivial conditions or
   state considerations), then consider using `--format=json` and using
   `jq` (or a script) to perform your filtering.

     As a starting point, here's how you can use `jq` to filter `zizmor`'s
     JSON output to only results that are marked as "high confidence":

     ```bash
     zizmor --format=json-v1 ... | jq 'map(select(.determinations.confidence == "High"))'
     ```

## Ignoring results

`zizmor`'s defaults are not always 100% right for every possible use case.

If you find that `zizmor` produces findings that aren't right for you
(and **aren't** false positives, which should be reported!), then you can
choose to *selectively ignore* results via either special ignore comments
*or* a `zizmor.yml` configuration file.

### With comments

!!! note

    Ignore comment support was added in `v0.6.0`.

Findings can be ignored inline with `# zizmor: ignore[rulename]` comments.
This is ideal for one-off ignores, where a whole `zizmor.yml` configuration
file would be too heavyweight.

Multiple different audits can be ignored with a single comment by
separating each rule with a comma, e.g.
`# zizmor: ignore[artipacked,ref-confusion]`.

To ignore a single `template-injection` finding:

```yaml title="example.yml"
run: | # zizmor: ignore[template-injection]
  echo "${{ github.event.issue.title }}"
```

Ignore comments can also have a trailing explanation:

```yaml title="example.yml"
run: | # zizmor: ignore[template-injection] i promise this is safe
  echo "${{ github.event.issue.title }}"
```

!!! important

    An ignore comment can be placed anywhere in any span identified by a finding,
    **so long** as it can be identified as a YAML comment. In particular,
    this means that you **can't** place an ignore comment in the middle of a string
    or a block literal. For example, the following does not work:

    ```yaml title="example.yml"
    # this is not suppressed, since the comment is actually part of the string
    run: |
      echo "${{ github.event.issue.title }}" # zizmor: ignore[template-injection]
    ```

    To fix this, you should place the ignore comment outside of the string,
    e.g. directly above it:

    ```yaml title="example.yml"
    run: | # zizmor: ignore[template-injection]
      echo "${{ github.event.issue.title }}"
    ```

### With `zizmor.yml`

When ignoring multiple findings (or entire files), a `zizmor.yml` configuration
file is easier to maintain than one-off comments.

Here's what a `zizmor.yml` file might look like:

```yaml title="zizmor.yml"
rules:
  template-injection:
    ignore:
      - safe.yml
      - somewhat-safe.yml:123
      - one-exact-spot.yml:123:456
```

Concretely, this `zizmor.yml` configuration declares three ignore rules,
all for the [`template-injection`](./audits.md#template-injection) audit:

1. Ignore all findings in `safe.yml`, regardless of line/column location
2. Ignore *any* findings in `somewhat-safe.yml` that occur on line 123
3. Ignore *one* finding in `one-exact-spot.yml` that occurs on line 123, column 456

More generally, the filename ignore syntax is `workflow.yml:line:col`, where
`line` and `col` are both optional and 1-based (meaning `foo.yml:1:1`
is the start of the file, not `foo.yml:0:0`).

To pass a configuration to `zizmor`, you can either place `zizmor.yml`
somewhere where `zizmor` [will discover it], or pass it explicitly via
the `--config` argument. With `--config`, the file can be named anything:

```bash
zizmor --config my-zizmor-config.yml /dir/to/audit
```

!!! important

    When using `--config`, only a single configuration file is used
    (instead of potentially discovering multiple configuration files,
    one per input source). As a result, using `--config` is
    **generally not recommended** unless auditing a single input source
    (file, directory, or remote repository).

!!! tip

    Starting with `v1.8.0`, you can use the `ZIZMOR_CONFIG` environment
    variable to specify a configuration file.

    `ZIZMOR_CONFIG=my-config.yml` is equivalent to
    `--config my-config.yml`.

[will discover it]: ./configuration.md#discovery

See [Configuration: `rules.<id>.ignore`](./configuration.md#rules-id-ignore) for
more details on writing ignore rules.

### Disabling ignores

!!! tip

     `--no-ignores` is available in `v1.25.0` and later.

Sometimes it's useful to disable ignores entirely. For example, if you're
reviewing a repository that someone else has previously triaged with `zizmor`,
you may want to temporarily disable their ignore rules to get a more complete
picture of the repository's security posture.

To do this, you can pass `--no-ignores` during audits:

```bash
zizmor --no-ignores example/example
```

## Caching between runs

!!! tip

    Persistent caching (between runs of `zizmor`) is available in `v0.10.0` and later.

!!! warning

    Caches can contain sensitive metadata, especially when auditing private
    repositories! Think twice before sharing your cache or reusing
    it across machine/visibility boundaries.

`zizmor` caches HTTP responses from GitHub's REST APIs to speed up individual
audits. This HTTP cache is persisted and re-used between runs as well.

By default `zizmor` will cache to an appropriate user-level caching directory:

* Linux and macOS: `$XDG_CACHE_DIR` (`~/.cache/zizmor` by default)
* Windows: `~\AppData\Roaming\woodruffw\zizmor`.

To override the default caching directory, pass `--cache-dir`:

```bash
# cache in /tmp instead
zizmor --cache-dir /tmp/zizmor ...
```

## Other GitHub hosts

!!! warning

    Support for custom GitHub Enterprise instances is provided on a **best-effort**
    basis. If you're a corporate user of `zizmor` who is interested in improving
    `zizmor`'s GitHub Enterprise support, please
    [let us know](https://github.com/zizmorcore/zizmor/issues/new)
    and [consider sponsoring](./index.md#sponsoring) development in this area!

`zizmor` supports GitHub instances other than `github.com`.

To use it with your [GitHub Enterprise](https://github.com/enterprise) instance
(either cloud or self-hosted), pass your instance's domain with `--gh-hostname`
or `GH_HOST`:

```bash
zizmor --gh-hostname custom.example.com ...

# or, with GH_HOST
GH_HOST=custom.ghe.com zizmor ...
```

## Limitations

`zizmor` can help you secure your CI/CD setup by finding common,
well-understood flaws.

However, like all tools, `zizmor` is **not a panacea**, and has
fundamental limitations that must be kept in mind. This page
documents some of those limitations.

### `zizmor` is a _static_ analysis tool { #static-analysis }

`zizmor` is a _static_ analysis tool. It never executes any code, nor does it
have access to any runtime state.

In contrast, many CI/CD systems (like GitHub Actions) are extremely dynamic,
and can be influenced by inputs that can only be inspected at runtime.

For example, here is a workflow where a job's matrix is generated
at runtime by a previous job, making the matrix impossible to
analyze statically:

```yaml
build-matrix:
  name: Build the matrix
  runs-on: ubuntu-latest
  outputs:
    matrix: ${{ steps.set-matrix.outputs.matrix }}
  steps:
    - id: set-matrix
      run: |
        echo "matrix=$(python generate_matrix.py)" >> "${GITHUB_OUTPUT}"

run:
  name: ${{ matrix.name }}
  needs:
    - build-matrix
  runs-on: ubuntu-latest
  strategy:
    matrix: ${{ fromJson(needs.build-matrix.outputs.matrix) }}
  steps:
    - run: |
        echo "hello ${{ matrix.something }}"
```

In the above, the expansion of `${{ matrix.something }}` is entirely controlled
by the output of `generate_matrix.py`, which is only known at runtime.

In such cases, `zizmor` will err on the side of verbosity. For example,
the [template-injection](./audits.md#template-injection) audit will flag
`${{ matrix.something }}` as a potential code injection risk, since it
can't infer anything about what `matrix.something` might expand to.

### `zizmor` audits workflow and action _definitions_ only { #definitions-only }

`zizmor` audits workflow and action _definitions_ only. That means the
contents of `foo.yml` (for your workflow definitions) or `action.yml` (for your
composite or Docker action definitions).

In practice, this means that `zizmor` does **not** analyze other files
referenced by workflow and action definitions. For example:

```yaml
example:
  runs-on: ubuntu-latest
  steps:
    - name: step-1
      run: |
        echo foo=$(bar) >> $GITHUB_ENV

    - name: step-2
      run: |
        # some-script.sh contains the same code as step-1
        ./some-script.sh
```

`zizmor` can analyze `step-1` above, because the code it executes
is present within the workflow definition itself. It *cannot* analyze
`step-2` beyond the presence of a script execution, since it doesn't
audit shell scripts or any other kind of files.

More generally, `zizmor` cannot analyze files indirectly referenced within
workflow/action definitions, as they may not actually exist until runtime.
For example, `some-script.sh` above may have been generated or downloaded
outside of any repository-tracked state.

### YAML anchors { #yaml-anchors }

!!! warning "Beta"

    `zizmor`'s support for YAML anchors is currently in **beta**.
    If you encounter a problem, please
    [file a bug report](https://github.com/zizmorcore/zizmor/issues/new).

`zizmor` tries very hard to present *useful* source spans in its audit
results.

To do this, `zizmor` needs to know a lot of about the inner workings
of the YAML serialization format that its inputs are expressed in.

YAML is a complicated serialization format, but typical inputs *mostly* use
a tractable subset of it. One conspicuous exception to this is
[YAML anchors](https://yaml.org/spec/1.2.2/#3222-anchors-and-aliases),
which GitHub has
[decided to support](https://github.blog/changelog/2025-09-18-actions-yaml-anchors-and-non-public-workflow-templates/)
in workflow and action definitions as of September 2025.

Anchors make `zizmor`'s analysis job *much* harder, as they introduce a
layer of (arbitrarily deep) indirection and misalignment between the
deserialized object model (which is what `zizmor` analyzes) and the source
representation (which `zizmor` spans back to).

If you're having issues with inputs containing anchors, see
[Troubleshooting - YAML anchors].

[Troubleshooting - YAML anchors]: ./troubleshooting.md#yaml-anchors

### Parallel steps { #parallel-steps }

!!! tip

    Support for parallel steps is available in `v1.27.0` and later.

!!! warning "Experimental"

    `zizmor`'s support for parallel steps is currently **experimental**.
    You will probably encounter bugs if you run `zizmor` on a GitHub Actions
    workflow that uses parallel steps; please
    [report any issues you have](https://github.com/zizmorcore/zizmor/issues/new)!

As of June 2026, GitHub Actions [supports parallel steps]. This support adds
several news features to Actions's syntax, most notably a `#!yaml parallel:` block
under which steps can be defined to make them run in parallel, rather than
the default of serial execution.

`zizmor` makes an effort to handle parallel steps in the same way that serial
steps are handled. In other words: anything that _would_ be flagged in a serial
step _should_ also be flagged if nested inside a `#!yaml parallel:` block.

[supports parallel steps]: https://github.blog/changelog/2026-06-25-actions-steps-can-now-be-run-in-parallel/
