Fix GitHub Actions "GITHUB_TOKEN Permission Denied" Errors
Quick Answer
- Root cause:
GITHUB_TOKENdefaults to read-only permissions in GitHub Actions, so any write operation (pushing commits, publishing packages, creating releases) fails with permission errors. - First check: Look at your workflow's job-level
permissions:block. If it's missing, the token defaults to read-only for most scopes. - Minimal fix: Add
permissions: contents: write(or the specific scope you need) to the job that requires write access. - Environment boundary: This applies to all GitHub-hosted and self-hosted runners using the automatically-generated
GITHUB_TOKEN. For cross-repository operations, you must use a PAT or GitHub App token instead.
What Problem It Solves
GitHub Actions automatically injects a GITHUB_TOKEN secret into every workflow run. This token authenticates actions and scripts as the github-actions[bot] user. However, GitHub deliberately restricts this token to read-only permissions by default as a security measure.
When your workflow attempts to:
- Push commits back to the repository
- Create or edit releases
- Publish packages to GitHub Packages
- Modify issues or pull requests
- Delete or rename files
...the token lacks the required write scope, and the workflow fails with errors like:
remote: Permission to user/repo.git denied to github-actions[bot].
npm ERR! code E403: You do not have permission to publish "package-name".
Resource not accessible by integration
The solution is to explicitly declare the permissions your job needs using the permissions: key.
When This Error or Setup Appears
Permission-denied errors surface in several common scenarios:
| Scenario | Typical Error |
|---|---|
| Auto-commit generated files (build artifacts, updated lockfiles) | remote: Permission to user/repo.git denied |
| Publishing to GitHub Packages (npm, Docker, Maven) | npm ERR! code E403 |
| Creating releases or tags | Resource not accessible by integration |
| Using third-party actions that write to the repo | Error: Input required and not supplied: token |
| Cross-repository operations | Resource not accessible by integration |
The error appears at the point where the write operation executes, not at workflow startup. This makes debugging confusing because earlier steps may succeed before the failure occurs.
Minimal Working Configuration
Add a permissions: block to the job that needs write access:
YAMLname: Auto-commit build output on: push: branches: [main] jobs: build-and-commit: runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v4 - name: Build run: npm run build - name: Commit changes run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add dist/ git commit -m "Update build output" git push
For publishing packages:
YAMLjobs: publish: runs-on: ubuntu-latest permissions: packages: write contents: read steps: - uses: actions/checkout@v4 - name: Publish to GitHub Packages run: npm publish env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Parameters and Environment Variables
The permissions: key accepts these scopes at both workflow and job level:
| Scope | Grants access to |
|---|---|
actions | Workflow management (cancel, approve, etc.) |
checks | Check runs and check suites |
contents | Repository contents (push, delete, edit files) |
deployments | Deployments |
discussions | Repository discussions |
issues | Issues and issue comments |
packages | GitHub Packages |
pages | GitHub Pages |
pull-requests | Pull requests and PR comments |
security-events | Code scanning alerts |
statuses | Commit statuses |
Each scope accepts read, write, or none:
YAMLpermissions: contents: write packages: read issues: none
You can also set a default at the workflow level and override per job:
YAMLpermissions: contents: read jobs: build: permissions: contents: write # overrides workflow default for this job
The GITHUB_TOKEN itself is automatically available as ${{ secrets.GITHUB_TOKEN }} — you do not need to create or configure it.
Root Cause Analysis
GitHub introduced default read-only permissions for GITHUB_TOKEN in February 2023. Before this change, the token had broad write access by default. The change was a security hardening measure: most workflows only need read access, and granting write by default created unnecessary risk if a workflow was compromised.
The token's permissions are determined by:
- Workflow-level
permissions:block — sets defaults for all jobs - Job-level
permissions:block — overrides workflow defaults for that job - If neither is specified — the default is read-only for all scopes (except
actionsandpackages, which default tonone)
The token is scoped to the current repository only. It cannot access other repositories, even within the same organization. This is why cross-repository operations fail even with permissions: contents: write declared.
Common Errors and Fixes
Error: remote: Permission to user/repo.git denied to github-actions[bot].
Cause: The job lacks contents: write permission.
Fix: Add to the job:
YAMLpermissions: contents: write
Error: npm ERR! code E403: You do not have permission to publish "package-name".
Cause: The job lacks packages: write, or NODE_AUTH_TOKEN is not set.
Fix:
YAMLpermissions: packages: write
And ensure your publish step sets:
YAMLenv: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Error: Resource not accessible by integration
Cause: The token is trying to access a resource outside its scope — either another repository, an organization-level resource, or a scope not declared in permissions:.
Fix: For same-repository operations, add the required scope. For cross-repository operations, you must use a PAT or GitHub App token instead:
YAML- uses: actions/checkout@v4 with: token: ${{ secrets.MY_PAT }}
Error: Error: Input required and not supplied: token
Cause: An action requires an explicit token parameter, but none was passed.
Fix: Pass the token explicitly:
YAML- uses: some/action@v1 with: token: ${{ secrets.GITHUB_TOKEN }}
Production Notes and Security Checks
Limitations of GITHUB_TOKEN
- Repository-scoped only: Cannot access other repositories in the same organization.
- Cannot read secrets: Use
${{ secrets.MY_SECRET }}to inject secrets explicitly. - Auto-expires: The token is valid only for the duration of the workflow run.
- No audit trail: The token acts as
github-actions[bot], making it harder to trace which workflow performed an action.
Security Recommendations
- Apply least privilege: Grant only the scopes each job needs. Avoid blanket
permissions: write-all. - Use job-level permissions: Override workflow defaults per job to minimize exposure.
- Never log the token: Avoid
echo ${{ secrets.GITHUB_TOKEN }}in your workflow. - Prefer GitHub App tokens for cross-repo work: GitHub App Installation Access Tokens (IATs) provide finer-grained control and automatic rotation compared to PATs.
- Review workflow permissions regularly: Audit your workflows periodically to ensure permissions match current needs.
When to Use PAT or GitHub App Instead
| Need | Recommended Token |
|---|---|
| Single repository operations | GITHUB_TOKEN |
| Cross-repository operations | PAT or GitHub App IAT |
| Organization-level resources | GitHub App IAT |
| Long-lived automation outside Actions | PAT (with careful management) |
FAQ
Q: Why is my GITHUB_TOKEN read-only by default?
A: GitHub changed the default to read-only in February 2023 as a security hardening measure. This prevents compromised workflows from performing destructive write operations. You must explicitly declare write permissions using the permissions: key.
Q: What's the difference between GITHUB_TOKEN and a PAT?
A: GITHUB_TOKEN is automatically generated per workflow run, scoped to the current repository, and expires when the run ends. A PAT is a user-created, long-lived token that can access multiple repositories and organizations but requires manual management and poses a higher leak risk. For cross-repository or organization-level operations, prefer a GitHub App Installation Access Token.
Q: How do I debug GITHUB_TOKEN permission issues?
A: First, check the job's permissions: block in your workflow file. Then examine the Actions run log — the error message usually identifies the missing scope. You can also verify repository-level permission settings via the GitHub API. If the error persists, test the same operation locally with a token that has known permissions to isolate whether the issue is token-scope-related.