GitHub Actions GITHUB_TOKEN cannot access another repository in CI
A GitHub Actions GITHUB_TOKEN cannot access another repository failure does not announce itself: GitHub returns the same answer it gives for a repository that was never created. The token is an installation token for one repository, and outside it there is nothing to refuse.

What this error means
A checkout, a submodule fetch or an API call against a second repository fails, while every call against the workflow's own repository succeeds in the same job. The message is about absence rather than permission: git reports that the repository was not found, and the REST API returns 404. Nobody is told that a boundary was crossed, so the first suspicion is a typo in the repository name, and the name is usually correct.
remote: Repository not found.
fatal: repository 'https://github.com/***/***.git/' not found
Error: Process completed with exit code 128.There is no message for this, and that is deliberate
Searching for a sentence in which GitHub explains that the token stopped at the repository boundary is time wasted, because no such sentence is emitted. We looked: the phrasing that circulates, about the token being scoped to this repository and therefore unable to access another, appears only in prose that people write in issues and pull request descriptions. Every occurrence we could verify was somebody explaining the rule in their own words. None of them was a log line, and a control search for messages that do exist returned them immediately.
What GitHub returns instead is 404 for the REST API and Repository not found. for git. That is the standard behavior for a private resource the caller cannot see, and it is chosen so that a 403 cannot be used to confirm that a private repository exists. The cost is that the message you get is indistinguishable from a genuine typo.
Common causes
A step checks out a second private repository with the default token
The common one. A build that needs a shared library, a config repository or a private submodule adds a second checkout and leaves token unset, so the automatic token is used and the clone reports the repository as missing.
Submodules are private and the parent checkout enables them
Subtler, because the failing name is a submodule rather than anything written in the workflow. submodules: true makes checkout fetch each one with the same credential, and a private submodule in another repository stops the whole checkout, taking every job that depends on it.
An API call or a repository dispatch targets another repository
A workflow that triggers a downstream build, opens an issue elsewhere or reads another repository's releases gets 404 from every one of those calls. Because 404 reads as a wrong URL, the endpoint gets rewritten several times before the credential is suspected.
The target repository was made private after the workflow was written
Worth checking when nothing in the workflow changed. A public dependency that becomes private turns a working anonymous read into a not-found, and the commit that broke the build is in a different repository from the one that is failing.
How to fix it
Establish that the boundary is the cause before changing credentials
- Confirm the repository name resolves in a browser while signed in.
- Check whether the same job succeeds against its own repository, which shows the token is working.
- If the target is public and the call is a read, the token is not the problem and something else is.
Use a GitHub App installation token for the durable version
Install an app on both repositories and mint an installation token in the job. The token is scoped to the installation rather than to one repository, it expires quickly, and it is not tied to a person who may leave. This is the option that survives contact with an organization.
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: shared-library
- uses: actions/checkout@v5
with:
repository: ${{ github.repository_owner }}/shared-library
token: ${{ steps.app-token.outputs.token }}
path: shared-libraryUse a fine-grained personal access token for the quick version
Select the target repository explicitly and grant only the permission the call needs. It works immediately and carries a person's access, which is its weakness: the build breaks when that person's access changes. Expect a differently worded refusal if the permission is too narrow, because the token type changes the message.
Avoid the second repository where you can
Publishing the shared code as a package, or vendoring the small piece that is actually needed, removes a credential from the workflow entirely. For submodules specifically, consider whether the dependency has to be a submodule rather than a versioned artifact.
Which answer each kind of call gives
The shape of the refusal depends on what you were doing, and only one of the four says anything about permission at all.
| What the job tried | Answer when the target is private | Answer when the target is public |
|---|---|---|
actions/checkout of a second repository | Repository not found. and exit 128 | Succeeds, because public code needs no authorization to read |
| A read through the REST API | 404 Not Found | Succeeds |
| A write through the REST API | 404 Not Found | 403 Resource not accessible by integration |
| A submodule fetch during checkout | Repository not found. for the submodule only | Succeeds |
Why the token cannot be widened
The permissions block does not have a repository axis. It tunes what the token may do inside the repository running the workflow, and GitHub documents the limit plainly: "The token's permissions are limited to the repository that contains your workflow." That is a property of the token itself, which is an installation access token for the Actions app on that one repository, rather than a policy applied on top of it.
So reaching a second repository always means bringing a second credential. There is no block to write, no flag to set, and no organization setting that extends the automatic token across repositories. Time spent looking for one is the most common way this failure consumes an afternoon.
Why there is no recorded run on this page
The interesting property of this failure is an absence: the log does not distinguish a repository you may not see from a repository that is not there. A recorded run can only exhibit one of those two cases at a time, and whichever we showed, the log would look exactly like the other. Demonstrating the ambiguity would require asserting what our own account can and cannot see, which is a claim about our permissions rather than about yours. The excerpt above is quoted from an issue on the checkout action, where the repository name is masked by the runner.
How to prevent it
- Treat every cross-repository step as needing its own credential, decided when the step is written rather than when it fails.
- Give each extra checkout its own
path, so a failure names the repository you meant. - Prefer an app installation token over a personal one for anything an organization depends on.
- When a dependency goes private, search for workflows that read it before the next build does.
Frequently asked questions
Does GitHub print a message saying the token cannot reach another repository?
Repository not found. from git.Can I add the other repository to the permissions block?
Why do I sometimes get 403 instead of 404?
Why did my checkout fail on a submodule I did not name?
submodules: true fetches each submodule with the same credential that cloned the parent. A submodule pointing at a private repository in another namespace is a cross-repository read, so it returns not-found and stops the checkout, even though the workflow only ever names the parent repository.