Diffinery
Actions: GITHUB_TOKEN permissions
Docs / Actions

GITHUB_TOKEN permissions

What a workflow job's GITHUB_TOKEN may do on Diffinery, how permissions are resolved, the repository default, and the rules no permission can lift.

Every workflow job gets its own token, GITHUB_TOKEN, that works against Diffinery's REST API and git server. What the token may do follows GitHub's permissions: model: each API endpoint needs a permission scope, and the job's effective permissions: decide which scopes the token has. This page explains how those permissions are worked out, how to change the repository's default, and the rules that apply whatever a workflow asks for.

The job token

The token is available as secrets.GITHUB_TOKEN, github.token and, under a Diffinery name, secrets.DIFFINERY_TOKEN. It isn't an environment variable unless you set one:

- run: gh api repos/${{ github.repository }}
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  • It works only while the job runs. When the job completes, the token stops working, and it never works more than 30 minutes past the job's timeout (counted from when the job started), even if the job is never marked complete.
  • It acts in the repository as diffinery-actions[bot], so issues, comments and labels it creates show that name.
  • Its role is write when any scope other than id-token is write, and read otherwise. It never has the maintain or admin role, so branch protection always applies to it.
  • Each request is then checked against the scope its endpoint needs (see What each endpoint needs).

How permissions are resolved

A job's effective permissions come from the first of these that exists:

  1. The job's own permissions: (jobs.<id>.permissions). It replaces the workflow's, it isn't merged with it.
  2. The workflow's top-level permissions:.
  3. The repository default, set by an admin (see The repository default).

Inside a reusable workflow, the calling job's permissions take the place of the repository default and also cap everything the called workflow declares. See Reusable workflows.

Scopes

permissions: takes these scopes, the same as GitHub's:

ScopeWhat it covers on Diffinery
actionsWorkflows, runs, jobs, logs, artifacts and caches (/actions/...), including starting a workflow_dispatch run
attestations/attestations
checksCheck runs and check suites
contentsGit fetch and push, files, commits, branches, tags, releases and release assets, compare, archives, commit comments, repository_dispatch, merging pull requests
deploymentsDeployments, deployment statuses, and reading environments
discussions/discussions
id-tokenRequesting an OIDC token. Only write (or none) has a meaning.
issuesIssues, issue comments, labels, milestones and assignees
modelsAccepted for compatibility. Only read or none.
packagesThe container registry
pages/pages
pull-requestsPull requests, reviews and review comments, and issue endpoints used on pull requests
repository-projects/projects
security-events/code-scanning, /secret-scanning, /dependabot
statusesCommit statuses

Each scope takes read, write or none. write includes read. An unknown scope gives the warning unknown permission scope '<scope>' when the workflow is parsed, and an invalid level fails the file with permission '<scope>' must be 'read', 'write' or 'none'.

Shorthand values

ValueResult
read-allEvery scope read, except id-token, which gets nothing.
write-allEvery scope write, except models, which stays read. Includes id-token: write.
{}Every scope none. The token can still read what anyone may read in a public repository.
A mappingThe scopes you list get the levels you give. Every scope you don't list is none.

Two scopes have special rules:

  • id-token: read grants nothing. OIDC needs id-token: write.
  • models: write is lowered to read.

Any other value of permissions: fails the file with permissions must be 'read-all', 'write-all' or a mapping of scope: read|write|none.

Examples

A workflow that only reads code, with one job that comments on pull requests:

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm test

  comment:
    needs: test
    runs-on: ubuntu-latest
    permissions:
      contents: read        # the job's map replaces the workflow's, so repeat contents
      pull-requests: write
    steps:
      - uses: actions/github-script@v7
        with:
          script: |
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: "Tests passed.",
            });

A release job that pushes a tag and uploads release assets needs contents: write. A job that pushes an image needs packages: write, and because naming any scope sets the others to none, it also needs contents: read to check out a private repository:

jobs:
  image:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

The repository default

When neither the job nor the workflow sets permissions:, the job gets the repository default. It's one of two settings, the same as GitHub's "Workflow permissions" setting:

default_workflow_permissionsToken permissions
readcontents: read and packages: read. Every other scope none.
writeEvery scope write (models read), except id-token, which gets nothing.
RepositoryDefault
Created after this setting was addedread, and pull request creation and approval off
Created beforewrite, and pull request creation and approval off

A second setting, can_approve_pull_request_reviews, decides whether job tokens may open pull requests and approve or request changes on them (see Creating and approving pull requests). It's false by default.

A third, agent_runs_use_secrets (a Diffinery addition), decides whether runs of code an agent wrote get the repository's and organization's secrets. It's false by default; see Which runs receive secrets.

Read and change the setting

In the web UI, open the repository's Settings → Actions tab (/{owner}/{repo}/settings/actions). Under Workflow permissions, pick Read repository contents and packages permissions or Read and write permissions, check or clear Allow GitHub Actions to create and approve pull requests and Give runs of agent code the repository's and organization's secrets, and click Save. You see "Actions settings saved." The tab needs the admin role on the repository.

You can also read and change it with the REST API or the CLI. Both need the admin role on the repository, and only people can change it: an agent's token gets 403 only people can change what workflows are allowed to do.

Read the current setting:

GET /api/v3/repos/acme/website/actions/permissions/workflow
Authorization: Bearer dfy_...
{
  "default_workflow_permissions": "read",
  "can_approve_pull_request_reviews": false,
  "agent_runs_use_secrets": false
}

Change it with PUT. Send any of the fields; a field you leave out keeps its value. The response is 204 No Content:

curl -fsS -X PUT https://diffinery.com/api/v3/repos/acme/website/actions/permissions/workflow \
  -H "Authorization: Bearer dfy_..." \
  -H "Content-Type: application/json" \
  -d '{"default_workflow_permissions": "write", "can_approve_pull_request_reviews": true}'

With the CLI (install it with curl -fsSL https://diffinery.com/install | bash, then diffinery login):

diffinery api GET /repos/acme/website/actions/permissions/workflow
diffinery api PUT /repos/acme/website/actions/permissions/workflow -f default_workflow_permissions=write

-f sends string values only. To change can_approve_pull_request_reviews, which is a boolean, send a JSON body with --input (a file, or - for stdin):

echo '{"can_approve_pull_request_reviews": true}' \
  | diffinery api PUT /repos/acme/website/actions/permissions/workflow --input -

Every change is recorded in the audit log as repo.actions_workflow_settings, with the new values.

Status and messageCause
403 admin access requiredYour role on the repository is below admin, or your token's scope doesn't reach admin.
403 only people can change what workflows are allowed to doThe request used an agent's token.
422 a JSON body is requiredThe body is missing or isn't JSON.
422 default_workflow_permissions must be read or writeAnother value was sent.
422 can_approve_pull_request_reviews must be a booleanA string such as "true" was sent instead of true.

Note: The setting is read when a job starts. Jobs that are already running keep the permissions they started with.

What each endpoint needs

Each request the token makes to /api/v3/repos/{owner}/{repo}/..., to the git server, or to a repository's web pages is matched to a scope by the first path segment after the repository. GET, HEAD and OPTIONS need read; other methods need write. Git fetches (git-upload-pack) and Git LFS downloads are reads even though they use POST.

Path after /repos/{owner}/{repo}Scope
/issues (list or create issues)issues
/issues/..., /labels, /milestones, /assigneesissues or pull-requests (either one is enough)
/pulls, /pulls/...pull-requests
/pulls/{number}/merge, /pulls/{number}/update-branch, /pulls/{number}/auto-mergecontents
/statuses/..., /commits/{sha}/status, /commits/{sha}/statusesstatuses
/check-runs, /check-suites, /commits/{sha}/check-runs, /commits/{sha}/check-suiteschecks
/deployments, reading /environmentsdeployments
/actions/...actions
/pagespages
/discussionsdiscussions
/code-scanning, /secret-scanning, /dependabotsecurity-events
/packagespackages
/projectsrepository-projects
/attestationsattestations
/contents, /commits, /compare, /git, /branches, /tags, /releases, /merges, /tarball, /zipball, /readme, /dispatches, /comments, /merge-upstream, /languages, /contributors, /statscontents
Git over HTTPS (/{owner}/{repo}.git/...), including pushcontents
Release asset uploads (/api/uploads/repos/{owner}/{repo}/releases/{id}/assets)contents
GET /repos/{owner}/{repo} and other reads not listed hereNone: repository metadata is always readable

Endpoints a job token can never use

Some endpoints are refused whatever the permissions, even with write-all:

  • /hooks, /collaborators, /keys, /invitations, /settings, /rulesets, /teams, /transfer, /autolinks, /vulnerability-alerts
  • /actions/secrets, /actions/variables, /actions/runners, /actions/permissions, /actions/oidc
  • Creating, changing or deleting environments, and environment secrets and variables
  • Branch protection (/branches/{branch}/protection)
  • Changing or deleting the repository itself (PATCH or DELETE /repos/{owner}/{repo})
  • Any write to a path not in the table above

Most of these endpoints need the admin role, which a job token never has, so the response is 403 admin access required. The others answer 403 this endpoint is not available to a workflow's GITHUB_TOKEN.

A job token can't re-run workflows either: POST /repos/{owner}/{repo}/actions/runs/{run_id}/rerun (and /rerun-failed-jobs, and POST /repos/{owner}/{repo}/actions/jobs/{job_id}/rerun) fails with 403 a workflow's GITHUB_TOKEN cannot re-run workflows.

The 403 response

A request whose scope the token lacks gets GitHub's message, with the scope it needed:

{
  "message": "Resource not accessible by integration (the workflow's GITHUB_TOKEN needs permissions: issues: write)",
  "documentation_url": "https://docs.github.com/rest"
}

When either of two scopes is enough, the message lists both, for example needs permissions: issues or pull-requests: write. Add the scope to the job's permissions: and run the job again.

Reading public repositories

What anyone may read in a public repository, a job token may read too, whatever its permissions:

  • In its own public repository, a read is allowed even with permissions: {} or contents: none.
  • It can read other public repositories on Diffinery, for example to check out a public dependency or clone a public submodule, but only read: any write is refused.
  • It can't see other private repositories at all, even ones with the same owner. A request gets 404. To check out another private repository, pass a personal or agent token from a secret as token:.

Workflow files

A job token never creates, changes or deletes files under .github/workflows/ or .diffinery/workflows/, whatever its permissions. This is GitHub's workflows permission, which a GITHUB_TOKEN can't have. The refusal reads:

refusing to allow a GitHub App to create or update workflow `.github/workflows/ci.yml` without `workflows` permission

It applies to:

  • git push: the ref update is rejected with that reason.
  • Commits through the API, such as the contents API.
  • Merging a pull request: the message appears as a merge blocker when the pull request changes a workflow file.

A push that creates a new branch is compared with where the branch left the default branch, so a new branch that only carries workflow changes already on the default branch is allowed. To change workflow files from automation, use a personal or agent token from a secret.

Creating and approving pull requests

By default a job token can't open pull requests, approve them or request changes on them. The request fails with:

GitHub Actions is not permitted to create pull requests. An admin can allow it in the repository's Actions settings ("Allow GitHub Actions to create and approve pull requests").

For reviews the message says approve instead of create. Commenting reviews are always allowed. To allow the rest, set can_approve_pull_request_reviews to true (see Read and change the setting); the job also needs pull-requests: write.

Even when allowed, an approval from a job token never counts toward a branch's required approvals. Only approvals by people count.

Pull requests from forks

A pull_request run for a pull request whose head is in another repository gets read access only: read-all at most, whatever the workflow asks for, and never id-token: write. It also gets no secrets: no repository, organization or environment secrets, only GITHUB_TOKEN, as on GitHub.

Diffinery has no forks of its own, so today this applies to pull requests imported from GitHub whose head was in a fork. Their head shows as owner:branch.

Reusable workflows

A reusable workflow never gets more than its caller:

  • A job in the called workflow without permissions: (and a called workflow without top-level permissions:) inherits the calling job's permissions.
  • Whatever the called workflow or its jobs declare is lowered, scope by scope, to what the calling job has.
  • The calling job's own permissions follow the usual order: its permissions:, then its workflow's, then the repository default.
# caller
jobs:
  deploy:
    uses: ./.github/workflows/deploy.yml
    permissions:
      contents: read
      id-token: write

Here the jobs in deploy.yml can have at most contents: read and id-token: write, even if deploy.yml asks for write-all.

Runs whose code came from an agent

A run is treated as agent code when:

  • an agent triggered it,
  • an agent last pushed the branch or tag it runs on (a merge that Diffinery lands for an agent through auto-merge counts as the agent's push),
  • for pull request and merge queue events, an agent opened the pull request or last pushed its head branch, or
  • off the default branch, the code it runs includes a commit an agent pushed that hasn't landed on the default branch yet. A person pushing on top of an agent's commits, tagging them, making a release from them or dispatching a workflow on them doesn't make that code a person's. Once a person merges the commits into the default branch (or a person lands them there), they count as that person's, since the default branch's runs go by who moved it.

A re-run of such a run keeps this, whoever starts the re-run, and so does a workflow_run run that follows such a run, since it acts on what that run produced (its artifacts and outputs). The jobs of these runs act as an agent, so the agent guardrails hold for code an agent wrote, even when a person triggered the run:

  • Pushes to the default branch or a protected branch are refused with agents may not push to protected or default branches; open a pull request, unless the branch's protection rule has Allow agents to push directly on.
  • Merges into those branches are blocked with agents may not merge into this branch; a human must merge, unless the rule has Allow agents to merge pull requests on. The token can still turn on auto-merge where the rule has Allow agents to land pull requests through auto-merge and a required check; that rule holds for every job token on those branches, agent code or not.
  • A pull request the job opens counts as agent-authored, so it needs at least one approval from a person, unless the base branch's rule waives approvals for agents' auto-merges.
  • A deployment the job records counts as the agent's, so environment secret rules for agents apply.
  • The jobs get no repository or organization secrets, only the job token, unless a repository admin who is a person turns on agent_runs_use_secrets (see Which runs receive secrets).

Runs started by the token

Like on GitHub, what a job does with its token starts no new workflow runs, so a workflow can't trigger itself in a loop. Pushes, pull requests, releases and merges made with a job token (including a later merge by an auto-merge the token turned on) don't start runs. The two exceptions, as on GitHub, are workflow_dispatch (a job with actions: write can start another workflow with POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches) and repository_dispatch (POST /repos/{owner}/{repo}/dispatches, which needs contents: write).

Differences from GitHub

AreaGitHubDiffinery
Changing the defaultSettings > Actions > General > Workflow permissionsSettings > Actions > Workflow permissions, or PUT /repos/{owner}/{repo}/actions/permissions/workflow
Organization and enterprise defaultsCan limit repositoriesNot available; each repository has its own setting
Agent codeNot a conceptRuns whose code came from an agent are held to the agent rules
Name in the repositorygithub-actions[bot]diffinery-actions[bot]
Extra nameNonesecrets.DIFFINERY_TOKEN

Troubleshooting

ProblemFix
Resource not accessible by integration (the workflow's GITHUB_TOKEN needs permissions: <scope>: write)Add that scope to the job's permissions:, or to the workflow's when the job has none. In a reusable workflow, the calling job needs it too.
A job that used to write now gets 403The repository's default is read (the default for new repositories), or the workflow added a permissions: map that leaves the scope out. Add the scope, or have an admin set default_workflow_permissions to write.
actions/checkout fails on a private repository after adding permissions:Naming any scope sets the others to none. Add contents: read.
this endpoint is not available to a workflow's GITHUB_TOKEN, or admin access required from a jobThe endpoint manages settings, secrets, variables, hooks, runners, environments or branch protection. Use a personal or agent token from a secret.
refusing to allow a GitHub App to create or update workflow ...Job tokens never change workflow files. Use a personal or agent token.
GitHub Actions is not permitted to create pull requests. ...An admin sets can_approve_pull_request_reviews to true, and the job needs pull-requests: write.
A job token's approval doesn't satisfy required reviewsOnly people's approvals count.
agents may not push to protected or default branches; open a pull request from a jobThe run's code came from an agent. Open a pull request for a person to review and merge.
A push from a job doesn't start the expected workflowEvents caused by a job token start no runs. Use workflow_dispatch with actions: write or repository_dispatch with contents: write, or a personal or agent token.
Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variableAdd id-token: write. id-token: read grants nothing.