# GitHub Actions working-directory does not exist on the runner

> When a GitHub Actions working-directory does not exist the step fails before your command runs. The message comes from .NET, not from Actions.

Source: https://latchkey.dev/learn/github-actions/github-actions-working-directory-does-not-exist  
Updated: 2026-09-20

When a GitHub Actions working-directory does not exist, the step fails while the runner is starting the shell process, before a single line of your script is read. The wording you get is not an Actions message at all, which is why searching for the phrase you expected returns nothing useful.

## What this error means

The step turns red almost instantly, with a single line of output and no trace of your script. There is no command echo, no group header for the run, and nothing from the program you were trying to invoke, because the failure happens while the process is being created rather than after it starts. The message names the interpreter, quotes an absolute path inside the workspace, and ends with an operating system error. The path in it is usually correct in the sense that it is the path you asked for; what is wrong is that nothing has created it yet. The step is often the first in a job, or the first after a job-level default was introduced, and it commonly runs before the repository has been checked out.

```Actions log, quoted from Vellum-KB#584
An error occurred trying to start process '/usr/bin/bash' with working directory '/home/runner/work/Vellum-KB/Vellum-KB/app'. No such file or directory
```

## Common causes

### A job default applied to a step that runs before checkout

The clearest version, and the one in both reports cited here. A default at job or workflow level applies to every run step underneath it, including guards, confirmations and anything else deliberately placed before the repository arrives. On a fresh runner the workspace exists and is empty, so the root resolves and the subdirectory does not.

### The directory is generated by a step that has not run yet

A build output, an extracted archive or a directory created by a generator. The path is right and the ordering is wrong, and moving the step one position later fixes it without touching the path at all.

### The relative path has one segment too many or too few

Relative values are resolved against the workspace root, which is the repository root after a default checkout. A path copied from a local shell that was already inside a subdirectory therefore gains a level that does not exist on the runner.

### Case or spelling differs from the repository

Linux runners are case sensitive and macOS runners are usually not, so a path that works on a developer machine and on one runner image can fail on another. In our experience this is the one that survives review, because the value looks correct to everyone reading it.

## How to fix it

### Give the pre-checkout steps a directory that always exists

The workspace root is created before the job starts, so naming it explicitly overrides the default for the one step that needs to run early. This is the fix applied in enterprise-onboarding-project#191, and it keeps the guard first rather than moving it after the clone.

```.github/workflows/deploy.yml (illustrative)
steps:
      - name: Confirm the target
        working-directory: ${{ github.workspace }}
        run: test "${{ inputs.confirm }}" = "yes"
      - uses: actions/checkout@v7
      - run: npm ci
```

### Move the default down to where it is true

A job-level default is a claim that every run step in the job belongs in that directory. When that is not true, put the value on the steps that need it, or split the job so each half has an honest default.

```.github/workflows/deploy.yml (illustrative)
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - name: Install
        working-directory: app
        run: npm ci
      - name: Build
        working-directory: app
        run: npm run build
```

### Print the workspace once and compare it with the message

1. Add a temporary step with no working directory that lists the workspace.
2. Compare the entry you expected against the path quoted in the failing message, character for character.
3. Check the case of every segment, since the same value can work on macOS and fail on Linux.
4. Delete the step once the path is right.

```.github/workflows/deploy.yml (illustrative)
- name: Show the workspace
        run: ls -la "$GITHUB_WORKSPACE"
```

### Create the directory before anything changes into it

When the path is produced by the job rather than by the repository, make it in a step that has no working directory of its own. A step cannot create the directory it is trying to start in, because the process fails before the script runs.

```.github/workflows/deploy.yml (illustrative)
- name: Prepare the output tree
        run: mkdir -p dist/reports
      - name: Write the report
        working-directory: dist/reports
        run: ./gen-report.sh
```

> A step that starts fine and then fails inside the script is a different problem: check which shell the step is running under on [GitHub Actions pipefail](/learn/github-actions/github-actions-shell-pipefail-masks-status).

## How to prevent it

- Keep pre-checkout steps out of any job that sets a working directory default.
- Prefer a working directory on the steps that need one over a default on the whole job.
- Create generated directories in an earlier step, never in the step that runs inside them.
- Match the case of every path segment to the repository, since one runner image will forgive it and another will not.

## A minimal workflow that produces it

This job is written for this page and has never been run. It sets a working directory for every run step in the job and then puts a guard step before the checkout, which is a sensible thing to want: fail fast on a bad input before spending time cloning.

The guard never gets to run. The default applies to it like it applies to everything else, and on a fresh runner the workspace is empty, so the directory named by the default does not exist yet. This is exactly the shape reported in enterprise-onboarding-project#191. Vellum-KB#584 is the same failure one step out: there the default sits at workflow level and the job it breaks has no checkout step in it at all.

```.github/workflows/deploy.yml (illustrative)
jobs:
  deploy:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: app
    steps:
      - name: Confirm the target
        run: test "${{ inputs.confirm }}" = "yes"
      - uses: actions/checkout@v7
      - run: npm ci
```

## The message is not from GitHub Actions

This is the detail that makes the failure hard to search for. The runner never checks whether the directory is there. It works out a path, puts it on the process start information, and asks the platform to create the process; when that fails, the exception carries a message composed by the .NET class library, and the runner prints it.

The format string is in `System.Diagnostics.Process` in dotnet/runtime, with three slots: the executable, the directory, and the operating system error. On Linux the third slot is the text for the underlying errno, which is where "No such file or directory" comes from. Knowing this is practical: it means the phrase to search for is the start of the sentence, not any wording about a working directory being invalid.

```dotnet/runtime, src/libraries/System.Diagnostics.Process
// dotnet/runtime, System.Diagnostics.Process Strings.resx
An error occurred trying to start process '{0}' with working directory '{1}'. {2}

// ProcessUtils.cs, which fills the middle slot
string directoryForException = string.IsNullOrEmpty(workingDirectory) ? Directory.GetCurrentDirectory() : workingDirectory;
```

## How the path in the message is built

The runner takes the value from the step, falls back to the job defaults when the step has none, and combines it with the workspace path from the repository context. The combination is a plain path join, which has one behavior worth knowing: an absolute second argument replaces the first rather than being appended to it.

That gives the resolution table below. Everything else is unchanged from what you wrote, so the path in the message is a faithful report of what the runner was asked for, and the question is only ever whether something has created it.

| What the step or defaults say | Path the runner asks for |
| --- | --- |
| nothing | the workspace root |
| app | the workspace root, then app |
| ./app | the workspace root, then app |
| ../app | the parent of the workspace root, then app |
| an absolute path | that absolute path, workspace ignored |
| ${{ github.workspace }} | the workspace root |

> Read from `ScriptHandler.cs` in actions/runner, which joins the workspace path from the repository context with the value from the step or the job defaults.

## The string you were probably searching for does not exist

There is no GitHub Actions error reading that a working directory does not exist. The documentation phrases the requirement as advice rather than as a message, in a tip beside the key: "Ensure the `working-directory` you assign exists on the runner before you run your shell in it."

So the page you want is this one, the message you have is the .NET one above, and any write-up quoting an Actions-branded sentence about a missing working directory is quoting something that was never emitted. It is also worth separating this failure from the one where the key appears to do nothing: a working directory on a step that uses an action rather than a script is ignored, and that is a different page.

> The key is documented as applying to a shell: "Using the `working-directory` keyword, you can specify the working directory of where to run the command."

## Why there is no recorded run on this page

This one does happen on a runner, which makes it worth saying clearly why there is still no log here. The library records a run when we have reproduced a failure on our own hardware and kept the output, and this batch has none. Nothing about the failure is transient, either: the directory is absent because of the order of the steps, so a retry produces the same line and there is nothing for a runner to repair. The quoted line comes from a public repository and is attributed on the block.

## FAQ

### Why does my step fail before printing anything at all?

Because the failure happens while the process is being created, not while your script is running. The runner hands the interpreter, the arguments and the directory to the platform, and the platform refuses because the directory is not there. Nothing of yours has been read at that point, so there is nothing to echo.

### Does a job-level working-directory default apply before checkout?

Yes, and that is the usual cause. A default applies to every run step in its scope regardless of position, so a guard or confirmation step placed deliberately before the clone inherits a path that the clone has not created yet. Set the workspace root on that one step to opt it out.

### Is a working-directory value relative to the repository or the workspace?

To the workspace, which is the repository root once a default checkout has run. The runner joins the two, and a value that is already absolute replaces the workspace rather than being appended to it, so an absolute path is used exactly as written.

### Which error text should I search for when this happens?

The opening of the sentence about trying to start a process, not any phrase about a directory being invalid. The wording comes from the .NET class library rather than from Actions, so there is no Actions-branded message to look for, and searching for one is why this failure feels undocumented.

## References

- [GitHub Actions: workflow syntax, jobs.<job_id>.steps[*].working-directory](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idstepsworking-directory)
- [dotnet/runtime: the process start error string and where it is composed](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.cs)
- [actions/runner: ScriptHandler.cs, how the working directory is resolved](https://github.com/actions/runner/blob/main/src/Runner.Worker/Handlers/ScriptHandler.cs)
- [Vellum-KB#584: a job default applied to a step with no checkout](https://github.com/masra91/Vellum-KB/issues/584)
- [enterprise-onboarding-project#191: a job-level default reaching a guard placed before checkout](https://github.com/dsl2022/enterprise-onboarding-project/issues/191)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
