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, or git tag.
  • Why it happens: The default GITHUB_TOKEN is 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_TOKEN typically only has read access 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_TOKEN does 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)

  1. Generate a PAT with the appropriate scopes (e.g., repo for repository access, read:org for organization access).
  2. Store this PAT as a GitHub Secret in your repository (e.g., REPO_PAT).
  3. 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 contents and potentially packages or deployments depending 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_TOKEN is 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

  1. Generate a PAT with the repo scope.
  2. Store it as a secret (e.g., CROSS_REPO_PAT).
  3. 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 the GITHUB_TOKEN it 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_TOKEN they 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.

Want structured learning?

Take the full Github-actions course →