> For the complete documentation index, see [llms.txt](https://docs.copia.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.copia.io/docs/actions/permissions.md).

# Permissions

Every Copia Actions job is given a short-lived token (`${{ secrets.COPIA_TOKEN }}`) that the job uses to talk back to Copia — for example to clone the repository, push commits, and comment on pull requests. The **token permissions** system controls what that token is allowed to do.

This page describes how to configure those defaults at the organization and repository level, how to narrow them further from inside a workflow, and what behavior to expect when something is denied.

{% hint style="info" %}
Default posture: out of the box, the Actions token can **read** code and releases on the job's own repository, and nothing else. Any workflow that needs to write to the repo (push commits, open PRs, upload releases) or read non-code units (issues, wiki) must either be granted broader permissions by an admin, or declare an explicit `permissions:` block in the workflow YAML.
{% endhint %}

## How permissions are resolved

When a job is dispatched to a runner, Copia computes the effective permission set for that job's token. Three inputs combine to produce the final answer:

{% stepper %}
{% step %}

### Workflow declaration (most specific)

If the workflow or job declares a `permissions:` block, that declaration is the starting point. Anything not listed defaults to no access.
{% endstep %}

{% step %}

### Repository and organization defaults

If the workflow does **not** declare `permissions:`, Copia uses the configured default mode (Permissive or Restricted) from the repository's Actions settings — or, if the repository does not override its owner, from the organization's Actions settings.
{% endstep %}

{% step %}

### Maximum-permission ceiling

Whichever starting point applies, the result is then clamped by the **maximum permissions** configured at the repository (when override is on) or at the organization. A workflow can never grant itself more than the ceiling allows.
{% endstep %}
{% endstepper %}

## The two default modes

Both repositories and organizations have a single "default token permission mode" setting with two choices:

| Mode           | What the token gets by default                                                                                 |
| -------------- | -------------------------------------------------------------------------------------------------------------- |
| **Restricted** | Read access to **Code** and **Releases** on the job's repository. No access to Issues, Pull Requests, or Wiki. |
| **Permissive** | Read and write access to all units on the job's repository (Code, Issues, Pull Requests, Wiki, Releases).      |

These defaults only apply when a workflow does not declare its own `permissions:` block. A workflow can always narrow further — it just can't go higher than the ceiling configured for the repository or organization.

{% hint style="warning" %}
**Restricted is the out-of-the-box default**, and it is stricter than the implicit "read everything" behavior most CI systems use. Workflows that read issues, pull requests, or the wiki via `COPIA_TOKEN` will receive `403 Forbidden` or `404 Not Found` responses unless their workflow declares the matching `permissions:` block, or an admin switches the repository (or its organization) to Permissive.
{% endhint %}

## Permission scopes

Permissions are granted per **unit**, where each unit corresponds to a feature of the repository. Each unit can be set to **No access**, **Read**, or **Write**.

| Unit          | Workflow YAML key        | Controls                                                   |
| ------------- | ------------------------ | ---------------------------------------------------------- |
| Code          | `code` or `contents`     | Cloning, pushing commits, reading file contents            |
| Issues        | `issues`                 | Reading and creating issues and issue comments             |
| Pull Requests | `pull-requests`          | Reading and creating PRs, leaving review comments, merging |
| Wiki          | `wiki`                   | Reading and updating the repository wiki                   |
| Releases      | `releases` or `contents` | Reading and creating releases and release assets           |

A few notes:

* **`contents:` is a shorthand** that applies to both `code` and `releases`. If you specify `code:` or `releases:` individually, the individual value wins.
* **GitHub-only and unused scopes** (`packages`, `projects`, `id-token`, `checks`, `pages`, `deployments`, `attestations`, `security-events`, `statuses`, `discussions`, etc.) are accepted for compatibility but have no effect in Copia. Workflows ported from GitHub Actions do not need to be edited to remove them.

## Configuration

### Organization-level (owner) settings

Organization owners can set the default mode and the maximum-permission ceiling for **every repository owned by the organization** that does not opt out.

Go to your organization on Copia, then **Settings → Actions → General**.

<figure><img src="/files/XOWWjoPQpsCvcZRvhYpw" alt=""><figcaption></figcaption></figure>

The page has two sections:

* **Default Token Permissions** — pick **Permissive** or **Restricted**. This is the mode every repository in the organization inherits unless it overrides.
* **Maximum Token Permissions** — optional ceiling. Enable **Customize maximum permissions** to expose the unit-by-unit table. Any permission requested by a workflow above the ceiling is clamped down.

When **Customize maximum permissions** is unchecked, the ceiling is effectively "write on everything" — i.e., the only thing limiting a workflow is the default mode and the workflow's own `permissions:` declaration.

You must be an organization **Owner** to view or modify these settings.

### Repository-level settings

Repository admins can configure the same knobs per-repository, and choose whether the repository follows the organization or overrides it.

Go to your repository on Copia, then **Settings → Actions → General**.

<figure><img src="/files/WF7ufwgo1b2Ewz0eyZMv" alt=""><figcaption></figcaption></figure>

The page has three parts:

* **Override owner-level configuration** — when **off** (the default), the repository inherits everything from the organization, and the mode/maximum controls below are disabled. When **on**, the repository's own configuration is used and the organization's settings are ignored for this repository.
* **Default Token Permissions** — same Permissive/Restricted choice as the organization page, applied only when override is on.
* **Maximum Token Permissions** — same per-unit ceiling table as the organization page, applied only when override is on.

You must be a repository **Admin** to view or modify these settings.

{% hint style="info" %}
Toggling override off does not delete the repository's saved configuration — it just stops applying it. If you turn override off and back on later, the previously saved mode and maximum table are still there.
{% endhint %}

## Declaring permissions in a workflow

The recommended pattern is to leave the org/repo default at **Restricted** and have each workflow opt into exactly the permissions it needs. This makes the workflow self-documenting and survives changes to the org default.

`permissions:` can be declared at the **workflow** level (applies to every job) or at the **job** level (applies only to that job). A job-level block fully replaces the workflow-level block for that job — they do not merge.

### Workflow-level

```yaml
name: Validate PRs
on: pull_request
permissions:
  contents: read
  pull-requests: write
jobs:
  validate:
    runs-on: windows10-S5K
    steps:
      - uses: actions/checkout@v4
      - name: Comment on the PR
        run: ./scripts/post-validation-comment.sh
```

### Job-level

```yaml
name: Release
on:
  push:
    tags: ['v*']
jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      contents: write # Push the release asset
      releases: write # Create the release
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/release.sh
```

### Shorthand values

Instead of a per-scope mapping, you can use a single keyword:

| Value                | Meaning                                       |
| -------------------- | --------------------------------------------- |
| `read-all`           | Read access on every supported unit           |
| `write-all`          | Read and write access on every supported unit |
| `{}` (empty mapping) | No access on any unit                         |

```yaml
permissions: read-all
```

Any other scalar value (e.g., `permissions: foo`) parses to "no access on any unit" — i.e., the safer interpretation.

### Per-scope values

Each scope accepts `read`, `write`, or `none`:

```yaml
permissions:
  contents: read
  issues: write
  pull-requests: write
  wiki: none
```

Scopes you do not list default to **no access** when you declare a `permissions:` block. This is intentional — declaring `permissions:` means "I am taking responsibility for the full permission set of this workflow."

## Re-runs and configuration changes

The permissions a job declared in its workflow YAML are stored on the job row at dispatch time, but the **clamp** against the org/repo configuration is re-applied every time the token is used. Concretely:

* If you tighten the org maximum after a job is already running, the running job's in-flight requests immediately start hitting the new, tighter ceiling.
* If you re-run a previous job, the workflow's declared permissions are reused as-is, but the org/repo ceiling that exists **at the time of the re-run** is what gets applied. You do not have to re-trigger the workflow from a push to pick up new ceiling values.

## Troubleshooting

<details>

<summary><strong>My workflow used to work and now I'm getting 403s on issue/PR comments.</strong></summary>

The fork's default mode is **Restricted**, which grants read on Code and Releases only. Either declare the permissions your workflow needs in its YAML:

```yaml
permissions:
  contents: read
  issues: write
  pull-requests: write
```

…or ask an admin to switch the organization (or just this repository) to **Permissive**.

</details>

<details>

<summary><strong>A workflow on a feature branch declares `permissions: write-all`, but the token still can't push.</strong></summary>

Check the organization's **Maximum Token Permissions**. A workflow can never request more than the ceiling allows — if the org's max for Code is set to **Read**, then `write-all` in the workflow is clamped down to read. Either raise the org maximum or, if appropriate, enable override on the repository and raise the repository's maximum instead.

</details>

<details>

<summary><strong>My workflow has `packages:` or `projects:` lines copied from GitHub Actions. Do I need to remove them?</strong></summary>

No. Copia does not have packages or projects features, so those scopes are parsed and ignored. Workflows ported from GitHub Actions do not need to have them stripped out.

</details>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.copia.io/docs/actions/permissions.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
