The GITHUB_TOKEN permission denied error happens when a GitHub Actions workflow tries to access a resource (like a repository, package registry, or secret) that its automatically generated GITHUB_TOKEN doesn’t have the necessary permissions for. This token is scoped to the repository where the workflow runs and has default read-only permissions for code and metadata.
Here are the common causes and their fixes:
1. Pushing to the Repository
Diagnosis: Your workflow is trying to push code changes, commit, or tag back to the repository. The default GITHUB_TOKEN only has read permissions for contents.
Cause:
- Action: A step in your workflow attempts
git push,git commit, orgit tag. - Why it happens: The default
GITHUB_TOKENis deliberately limited for security. Allowing write access by default would be a huge risk if a workflow was compromised.
Fix:
Modify your workflow file (.github/workflows/*.yml) to grant write permissions for contents.
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write # Grant write access to repository contents
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for git operations
- name: Commit and push changes
run: |
git config --global user.name 'GitHub Actions'
git config --global user.email 'actions@github.com'
git add .
git commit -m "Automated changes"
git push
Why it works: Explicitly declaring contents: write in the permissions block for the job elevates the GITHUB_TOKEN’s privileges for that specific job, allowing Git operations that modify the repository.
2. Publishing to GitHub Packages (npm, Docker, Maven, etc.)
Diagnosis: Your workflow fails when trying to publish an artifact to GitHub Packages (e.g., npm publish, docker push, mvn deploy).
Cause:
- Action: A step uses a package manager or tool to publish to a GitHub Packages registry.
- Why it happens: The default
GITHUB_TOKENtypically only hasreadaccess to packages. Publishing requires write access.
Fix:
Grant write permissions for packages.
jobs:
publish:
runs-on: ubuntu-latest
permissions:
packages: write # Grant write access to GitHub Packages
contents: read # Usually needed for checkout
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
registry-url: 'https://npm.pkg.github.com/'
- name: Install dependencies
run: npm ci
- name: Publish package
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Why it works: The packages: write permission allows the GITHUB_TOKEN to create and push new package versions to the GitHub Packages registry associated with your repository.
3. Reading Secrets from GitHub Secrets
Diagnosis: Your workflow fails when trying to access repository secrets (e.g., secrets.MY_API_KEY).
Cause:
- Action: A step tries to read a value from
secrets.VARIABLE_NAME. - Why it happens: By default, the
GITHUB_TOKENdoes not have access to read secrets. This is a critical security boundary.
Fix:
You cannot grant GITHUB_TOKEN access to repository secrets directly through the permissions block. Instead, you must use a Personal Access Token (PAT) or an Organization-level Installation Access Token (IAT) for these operations.
Option A: Using a PAT (for repository-level secrets)
- Generate a PAT with the appropriate scopes (e.g.,
repofor repository access,read:orgfor organization access). - Store this PAT as a GitHub Secret in your repository (e.g.,
REPO_PAT). - Use this secret in your workflow.
jobs:
access_secret:
runs-on: ubuntu-latest
steps:
- name: Use repository secret
run: echo "My API Key is ${{ secrets.MY_API_KEY }}"
env:
MY_API_KEY: ${{ secrets.MY_API_KEY }} # This is how you use it in a step
Note: The above example demonstrates reading a secret, which doesn’t require special token permissions. If the action that uses the secret needs elevated permissions (e.g., to push to a registry using credentials from the secret), then you’d grant those permissions as shown in other examples.
The actual "permission denied" for secrets usually arises when an action tries to use a secret and that action itself needs elevated permissions that the GITHUB_TOKEN doesn’t have. For example, if an action uses a secret to authenticate with an external service and that action needs to push to GitHub Packages, you’d combine the packages: write permission with the secret.
Option B: Using an Installation Access Token (IAT) (for organization-level secrets or more granular control) This is more complex and involves using the GitHub App installation token.
Why it works (for secrets): GITHUB_TOKEN is intentionally isolated from secrets. Secrets are designed to be injected into workflows via explicit secrets context, and the actions that use those secrets must have their own permissions granted if they perform privileged operations.
4. Creating/Managing Releases
Diagnosis: Your workflow fails when trying to create a GitHub Release.
Cause:
- Action: A step uses an action or command to create a release (e.g.,
actions/create-release). - Why it happens: Creating releases requires write access to
contentsand potentiallypackagesordeploymentsdepending on what the release includes.
Fix:
Grant write permissions for contents and deployments if applicable.
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write # Required to create releases
deployments: write # Often needed if the release is tied to a deployment
steps:
- uses: actions/checkout@v4
- name: Create GitHub Release
uses: actions/create-release@v1
id: create_release
with:
# ... release details ...
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Why it works: The GITHUB_TOKEN with contents: write permission can interact with the GitHub API to perform release-related actions like creating tags and release notes.
5. Accessing Other Repositories in the Same Organization
Diagnosis: Your workflow fails when trying to access resources (code, packages) in a different repository within the same organization.
Cause:
- Action: A step tries to clone, pull, or interact with another repository.
- Why it happens: The default
GITHUB_TOKENis scoped only to the repository where the workflow is running. It does not have inherent permissions for other repositories, even within the same organization.
Fix: Use a PAT or an IAT that has been granted access to the target repository.
Option A: PAT
- Generate a PAT with the
reposcope. - Store it as a secret (e.g.,
CROSS_REPO_PAT). - Use it in your checkout step:
jobs:
cross_repo_access:
runs-on: ubuntu-latest
steps:
- name: Checkout another repository
uses: actions/checkout@v4
with:
repository: owner/other-repo
token: ${{ secrets.CROSS_REPO_PAT }} # Use your PAT here
Option B: IAT This involves setting up a GitHub App and using its installation token.
Why it works: The PAT or IAT acts as an authenticated user or app that does have explicit permissions granted to the target repository, allowing the workflow to perform actions there.
6. Insufficient Permissions for a Specific Action
Diagnosis: A third-party action fails with a "permission denied" error when trying to access GitHub resources.
Cause:
- Action: A specific action within your workflow (e.g.,
my-org/my-action@v1) requires elevated permissions to perform its task, but theGITHUB_TOKENit receives doesn’t have them. - Why it happens: Many actions are designed to interact with GitHub’s API in privileged ways (e.g., updating PRs, creating issues, interacting with packages). They inherit the permissions of the
GITHUB_TOKENthey are given.
Fix:
Grant the necessary permissions at the job level in your workflow file. Examine the action’s documentation to understand what permissions it requires. Common ones include contents: write, packages: write, issues: write, pull-requests: write, deployments: write.
jobs:
run_privileged_action:
runs-on: ubuntu-latest
permissions:
contents: write # Example: If the action needs to push code
issues: write # Example: If the action creates issues
pull-requests: write # Example: If the action comments on PRs
steps:
- uses: actions/checkout@v4
- name: Run the privileged action
uses: some-org/some-action@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Why it works: By declaring the required permissions in the jobs.<job_id>.permissions block, you ensure that the GITHUB_TOKEN provided to all steps within that job has the necessary API access to fulfill the action’s requirements.
The next error you’ll likely encounter after fixing these is a rate limiting issue if you’re making too many API calls in a short period, or a configuration error within one of the specific tools (like npm or Docker) if the permissions were just one part of a larger setup problem.