No such file or directory @ rb_sysopen on a macOS runner
No such file or directory @ rb_sysopen is Ruby reporting a failed open call, with the C function name spliced into the middle of the sentence and the path it tried on the end. On a macOS runner the Ruby program raising it is more often Homebrew than anything in your repository, which changes both where you look and what you change.

What this error means
A step on macos-latest or a pinned macOS image dies with a sentence in three parts: the operating system's description of the error number, an @ and a function name, then a path. The path is the only part specific to your run, and it is the thing to read first. A path under Library/Caches/Homebrew/downloads means brew was pouring a bottle it thought it had already downloaded. A path under /Users/runner/work/<repo>/<repo> means something in your checkout or build tree is absent. A path outside both usually means a tool is looking somewhere a developer machine has and a runner does not. The runner then adds its own line, Error: Process completed with exit code 1, which is not part of the Ruby message.
==> Pouring boost--1.90.0.arm64_sequoia.bottle.tar.gz
Error: No such file or directory @ rb_sysopen - /Users/runner/Library/Caches/Homebrew/downloads/f0800259371e159e5eac9783f072a6c27951d50e7910b9d571be939c567d1604--boost--1.90.0.arm64_sequoia.bottle.tar.gz
Error: Process completed with exit code 1.How the sentence is built, and why the middle part is a C function name
Ruby builds this message in syserr_initialize in error.c. It starts from strerror for the error number, which is where No such file or directory comes from. If a path was supplied it appends " @ " and the function name, then " - " and the path. The trailing (Errno::ENOENT) you sometimes see is added later, by the default exception printer, which writes the exception class in parentheses after the message unless that class is anonymous.
The function name is not chosen by anyone. rb_syserr_fail_path is a macro in internal/error.h that expands to a call passing RUBY_FUNCTION_NAME_STRING, the compiler's name for the enclosing function, and rb_sysopen in io.c is the function that opens a file by path for File.open, File.read, IO.read and everything built on them. So the token after the @ tells you which kind of call failed, and rb_sysopen specifically means an open.
| Part of the message | Where it comes from | What it tells you |
|---|---|---|
No such file or directory | strerror on the error number | The error number was ENOENT |
@ rb_sysopen | The enclosing C function's own name | The failing call was an open, not a stat or a directory read |
- <path> | The path argument, as Ruby received it | A relative path here means the working directory decides |
(Errno::ENOENT) | The exception printer, not the message | Nothing extra; it is the class name |
Common causes
Homebrew went to pour a bottle that is not in its download cache
The dominant cause on GitHub-hosted macOS images, and the one people least expect because the failing program is Ruby but the project is not. The path names the Homebrew downloads directory and the bottle, and the line above it begins with ==> Pouring. Nothing in the repository caused it and nothing in the repository fixes it; the remedy is to make brew fetch again.
The path exists on a developer Mac and not on a runner
A config file that is gitignored, a credential the developer has in their home directory, or a tool installed by hand. The message gives the absolute path, and on macOS runners the /Users/runner prefix makes it obvious that the home directory is not the one the script was written for.
A relative path resolved against the wrong working directory
Ruby reports the path as it was handed over, so a relative path in the message means the process working directory decided the outcome. In our experience this is most common in steps that set working-directory on some steps and not on others, or that cd inside a multi-line script.
An earlier step never produced the file
The path is right, inside the workspace, and simply absent. A skipped step, a step that wrote elsewhere, or a file that only exists in another job. The failing step is where it surfaced, not where it went wrong.
How to fix it
Classify the path before you read the Ruby
- Take the path after the
-in the message. - Under
Library/Caches/Homebrew, this is a brew problem and the fix is a re-fetch. - Under
/Users/runner/work, ask which step was supposed to create it. - Anywhere else, or relative, the script is assuming a machine layout the runner does not have.
Make Homebrew fetch again rather than reuse its cache
Refresh the formula index and clear the stale download before reinstalling. Doing this only in the recovery path keeps normal runs fast, because a healthy cache is the reason most macOS jobs install quickly at all.
- name: Install with a clean download cache
run: |
brew update
brew cleanup --prune=all
brew install --force protobufMaterialise the file the step needs, from a secret or an earlier step
Where the missing path is a config or credential that legitimately does not live in the repository, write it before the failing step rather than teaching the tool to tolerate its absence. Create the directory first, because an open on a path inside a missing directory raises the same message.
- name: Write the signing config
env:
SIGNING_JSON: ${{ secrets.SIGNING_JSON }}
run: |
mkdir -p config
printf '%s' "$SIGNING_JSON" > config/signing.jsonRemove the working-directory question entirely
Anchor paths to the workspace rather than to wherever the process happens to be. GITHUB_WORKSPACE is set for every step and is the same for all of them, so a path built from it cannot drift when someone adds a cd.
- run: bundle exec ruby tools/report.rb "$GITHUB_WORKSPACE/config/app.json"On a macOS runner, the Ruby program is usually brew
Homebrew is written in Ruby, so every failure it has to open a file surfaces in exactly this form, and on GitHub-hosted macOS images brew is involved in most jobs whether or not the repository is a Ruby project. The signature is a path under Library/Caches/Homebrew/downloads with a long hash and the bottle file name after two dashes, immediately below a line beginning ==> Pouring.
That sequence means brew believed a bottle was already in its download cache and found nothing there when it went to open it. The cause is in the download step above, not in the pour: a fetch that failed, a cache the image shipped in an inconsistent state, or a formula whose bottle was replaced between the manifest fetch and the pour. Changing the pour is not an option, so the fix is to make brew fetch it again.
brew update
brew cleanup --prune=all
brew install --force boostPaths that only exist on the machine you wrote the script on
The second family is the classic one, and the macOS runner shape of it is distinctive because the home directory is /Users/runner and the checkout sits at /Users/runner/work/<repo>/<repo>, with the repository name appearing twice. A script that resolves a path relative to $HOME, or one that assumes a Gemfile, a keychain file or a signing profile is present, produces an absolute path in the message that says plainly it was looking somewhere the runner has nothing.
The related trap is a relative path. Ruby puts the path in the message exactly as it received it, so a relative path in the message tells you the answer depends on the process working directory, which in a workflow is whatever working-directory or a preceding cd last set. An absolute path removes the question entirely.
A file a previous step was supposed to produce
The third family is a path inside the workspace that is correct in every respect except that nothing created it. A packaging step that expects a built gem, a test that reads a fixture generated earlier, or a deploy that reads a file written by a job that did not run all produce a confident absolute path under /Users/runner/work.
This one is worth separating from the other two because the fix is never in the failing step. The question is which earlier step was meant to write that path and whether it ran, was skipped by an if, or wrote somewhere else.
Why there is no recorded run on this page
The message is assembled by the Ruby interpreter from an error number, a compiler-supplied function name and whatever path the calling program passed, so a run of ours would demonstrate a path we chose to delete. Worse, the most common macOS instance of it depends on the state of a Homebrew download cache baked into a runner image on a particular day, which we could only reproduce by breaking a cache on purpose and then presenting the result as though it were a natural failure. The assembly is read from the interpreter's own source and the failures are quoted from public runs that hit them without being asked to.
How to prevent it
- Build paths from
GITHUB_WORKSPACErather than from the current directory or the home directory. - Create required directories before writing into them, since an open inside a missing directory raises the same error.
- Keep a recovery path for Homebrew that clears the download cache, and use it only when an install fails.
- Check that every file a step reads is produced in the same job, or uploaded and downloaded as an artifact.
Frequently asked questions
What does the "@ rb_sysopen" part of the message mean?
rb_sysopen in io.c opens a file by path, so the token means the failing call was an open. A different failing call puts a different name there.Why is Homebrew raising a Ruby error on my macOS runner?
Errno::ENOENT. A path under Library/Caches/Homebrew/downloads below a ==> Pouring line means the bottle it expected in its download cache was not there.Where is the checkout on a GitHub-hosted macOS runner?
/Users/runner/work/<repo>/<repo>, with the repository name twice, and the runner's home directory is /Users/runner. A path in the message that starts anywhere else is a strong signal that the script was written against a developer machine.The message shows a relative path. Is that a bug?
GITHUB_WORKSPACE and the ambiguity disappears.Related guides
References
- ruby/ruby: error.c, syserr_initialize assembling the message from errno, function and path
- ruby/ruby: internal/error.h, the macro that supplies the function name
- ruby/ruby: io.c, rb_sysopen and the failure path it raises from
- apache/arrow#48809: the Homebrew bottle failure quoted here
- actions/runner-images: the macOS 15 arm64 image contents, including its Ruby and Homebrew versions
- GitHub Actions documentation