# dorny/paths-filter: conditional workflow steps in GitHub Actions

> A GitHub Action that turns the list of files a pull request or push touched into step-level conditions. It fits monorepos and CI jobs that should not run on every commit.

**dorny/paths-filter** — Conditionally run actions based on files modified by PR, feature branch or pushed commits

- Repository: https://github.com/dorny/paths-filter
- Stars: 3,340 · Forks: 388
- Language: TypeScript
- License: MIT
- Published: 2026-09-24 · Updated: 2026-09-24 · Language: en
- Canonical page: https://hysenlabs.com/en/projects/dorny-paths-filter

## The job-level gap that dorny/paths-filter fills

GitHub's built-in path filters sit on the workflow trigger. The README is explicit that they "don't allow this because they don't work on a level of individual jobs or steps." If a workflow fires at all, every job inside it runs, even the ones whose source directories were untouched. On a monorepo with a backend, a frontend and a docs site, a one-line README edit can drag the full integration suite along with it.

dorny/paths-filter moves the decision inside the run. The action inspects the set of modified files, matches them against named filters you define, and writes a boolean per filter that later steps and jobs can test with an if condition. The README frames the payoff plainly: run slow tasks like integration tests or deployments only for changed components, which "saves time and resources, especially in monorepo setups." That is the whole product. It is for teams whose CI bill or wall-clock time is dominated by jobs that had no reason to execute.

## How change detection differs across pull requests, pushes and merge queues

The mechanism is not one code path. It branches on the event that triggered the workflow, and the difference matters when you are debugging a filter that returns false.

On pull_request and pull_request_target events, the action calls the GitHub REST API to fetch the modified file list and compares against the pull request base branch. That path needs the pull-requests: read permission, and the README lists it as a requirement rather than a suggestion.

On push and other events, detection switches to git commands, which means the repository must already be checked out, typically with actions/checkout. For feature branches, the base input must not equal the branch that triggered the workflow, and changes are measured against the merge-base with the configured base branch or the default branch. For master, release or other long-lived branches where base equals the triggering branch, the comparison is against the most recent commit on the same branch before the push. When base is a commit SHA, the comparison is against that commit. When base is HEAD, changes are detected against the current HEAD and untracked files are ignored.

Merge queue is its own case: on merge_group, base and ref default to commit hashes from the event unless you set them, and detection again uses git, so checkout is required.

The matching itself is done by picomatch. The README notes that its dot option is set to true, so a glob will also match paths whose file or folder name starts with a dot. That is a real behavioural difference from many shell glob defaults and a common source of surprise when a filter unexpectedly matches .github or .env files.

## Installing dorny/paths-filter and running your first filtered job

There is no package to install. You reference the action inside a workflow file with a uses line, as the README's example does. The current major tag in the README is v4, and the release list shows v4.0.3 published on 2026-08-05.

The example below defines one filter named src and gates a later step on its output. The filter value is a YAML block where the key is the filter name and the list holds path expressions.

```yaml
- uses: dorny/paths-filter@v4
  id: changes
  with:
    filters: |
      src:
        - 'src/**'

  # run only if some file in 'src' folder was changed
- if: steps.changes.outputs.src == 'true'
  run: ...
```

After the action runs, steps.changes.outputs.src is the string 'true' or 'false', and the if condition is what skips the work. Quote your path expressions with ' or ", because the README warns that an unquoted expression starting with * produces an error.

On pull request workflows, add the permission the action needs:

```yaml
permissions:
  pull-requests: read
```

If you want to drive a matrix from the changed folders, the README lists a changes output for configuring a matrix job to run for each folder with changes. If you need the file list itself rather than a boolean, list-files supports shell, escape and csv formats.

For local runs with act, the README states that only an alternative runner image works because the default runner has no git binary, and gives the command act -P ubuntu-latest=nektos/act-environments-ubuntu:18.04.

## The *_files outputs are attacker-influenced on pull requests

This is the limitation worth reading twice. The README's security note says that ${FILTER_NAME}_files outputs contain filenames that may be attacker-influenced on pull requests, and instructs you not to interpolate them directly into a run: script with ${{ ... }}. Pass the value through env: and reference it from the shell instead.

That is not a hypothetical style preference. A pull request author controls the names of the files in their branch, and a filename is a string that lands in your shell if you splice it in with expression syntax. The action gives you the safe pattern, but it cannot enforce it. The failure mode belongs to your workflow, not to the action.

There is a second sharp edge around container jobs. Git can raise dubious ownership errors there, and the README says the action handles them automatically by retrying with a temporary HOME containing a safe.directory entry, the same technique actions/checkout uses. The caveat follows immediately: if fetching relies on credentials stored in HOME-relative files such as ~/.git-credentials or ~/.netrc, that workaround is not enough, and you must mark the repository as safe yourself in a step before the action with git config --global --add safe.directory "$GITHUB_WORKSPACE".

A third boundary is simply the wrong-tool case. If your workflow never checks out the repository, every git-based mode is unavailable to you, which rules out the push, merge queue and HEAD paths. And if all you need is to stop an entire workflow from firing on unrelated changes, the built-in trigger filters already do that with no extra step.

## dorny/paths-filter compared with a scripted git diff step

The obvious alternative is not another action. It is a shell step that runs git diff --name-only against the base ref and greps the result, then writes to $GITHUB_OUTPUT. That approach has no third-party dependency and no version tag to track. What it does not have is the event handling.

The action's value is concentrated in the branches described earlier: choosing the REST API for pull_request, computing the merge-base for feature branches, comparing against the previous commit when base equals the triggering branch, and defaulting base and ref from the event on merge_group. A hand-rolled diff step has to reproduce that logic, and getting the merge-base wrong on a feature branch is a silent error that shows up as a filter that never fires.

Against the built-in on: push paths filters, the difference is granularity, not capability. The built-in filters decide whether the workflow starts. dorny/paths-filter decides which steps and jobs inside a running workflow execute. Those compose: you can use both, with the trigger filter as a coarse gate and the action as the fine one.

Between the action's own major versions, the README records that v4 followed an update to Node 24 and labels it a breaking change. The release list also shows v3.0.4 published on 2026-08-05, the same day as v4.0.3, so the v3 line is still receiving releases rather than being abandoned.

## Maintenance, licence and what upgrading between major tags costs

The repository is not archived, and the last push was on 2026-08-05. Releases v4.0.3 and v3.0.4 landed on that same date, with v4.0.2 before them on 2026-07-02. Both the current and previous major lines have recent activity, which is the practical question when you pin a tag.

The project is MIT licensed, with the LICENSE file at the repository root and the same identifier in package.json. MIT imposes essentially no conditions on how you call the action from a workflow. It says nothing about the GitHub Actions platform itself, and it does not cover the picomatch dependency, which is a separate package with its own licence. That is a packaging fact, not legal advice; if your organisation audits dependency licences, picomatch is the other entry to check.

The upgrade cost is mostly the v3 to v4 jump. The README attributes v4 to a Node 24 update and marks it breaking, and package.json sets engines.node to >= 24. Moving a workflow from @v3 to @v4 is a one-line edit, but the runtime change is the kind of thing that surfaces in self-hosted runners whose Node version lags. Pinning to a major tag means you absorb future minor and patch releases automatically; pinning to a full version such as v4.0.3 means you do not. The repository ships a dist/ directory and a pack script using ncc, which is the standard shape for a JavaScript action, so there is no build step on your side.

## Conclusion

Adopt dorny/paths-filter if you run a monorepo and want per-component jobs gated on changed files, and you are willing to pin a major tag and keep checkout in place for push, merge queue and local modes. Skip it if you only need the built-in on: push paths filters, or if your workflows never check out the repository. Before rolling it out, verify that pull-requests: read is granted on PR workflows, that your filter globs are quoted, and that any use of the *_files outputs goes through env: rather than direct interpolation.

## FAQ

### What is dorny/paths-filter?

It is a GitHub Action that enables conditional execution of workflow steps and jobs based on the files modified by a pull request, a feature branch, or recently pushed commits. It exposes a boolean output per named filter that later steps test with an if condition.

### How can I filter paths in GitHub Actions with dorny/paths-filter?

Define a filters block on the action with a filter name and a list of path expressions, give the step an id, then gate later steps on steps.<id>.outputs.<filter> == 'true'. Path expressions are evaluated with picomatch, and the README recommends quoting them so expressions starting with * do not error.

### How do I exclude paths in dorny/paths-filter?

The README documents a some-with-excludes value for the predicate-quantifier input, which is the mechanism for combining matches with exclusions. The full path expression format is documented on the picomatch project page rather than in the README.

## Sources

- [dorny/paths-filter on GitHub](https://github.com/dorny/paths-filter)
- [Issues](https://github.com/dorny/paths-filter/issues)
- [License: MIT](https://github.com/dorny/paths-filter/blob/master/LICENSE)
- [README](https://github.com/dorny/paths-filter/blob/master/README.md)
- [Releases](https://github.com/dorny/paths-filter/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/en/projects/dorny-paths-filter
