> ## Documentation Index
> Fetch the complete documentation index at: https://docs.garnet.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Security and permissions

> What the Garnet action needs from your workflow, what runs on the runner and as whom, what leaves the runner, and what becomes public.

The action adds one step to a job. This page describes the grants that step asks for, what it installs on the runner and as whom, what it sends, and what can become public. You can check each of them against the workflow diff and the run log. Everything here describes `garnet-org/action` v2.3.0, the pinned release in the [quickstart](/quickstart).

## Workflow permissions

```yaml theme={null}
permissions: {}

jobs:
  build:
    permissions:
      contents: read
```

| Permission        | Required                 | What it does                                                                                                                                                                                                                                           |
| :---------------- | :----------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `contents: read`  | Yes                      | The minimum job token. The action uses it for `gh attestation verify` on the sensor release, to fetch the running workflow file through the GitHub API when there is no checkout, and to read the job's status when no Execution Profile was produced. |
| `id-token: write` | Only without `api_token` | Lets the action request a GitHub OIDC token and exchange it with Garnet. Not needed when you pass `api_token`.                                                                                                                                         |

The action needs no `pull-requests: write`, `contents: write`, `checks: write`, `statuses: write`, `actions: write`, or `secrets: inherit`. It posts no comment, no Check, sets no commit status, and writes nothing to the repository; that is a statement about this action, not about the App or any other consumer. The pull request comment is the [GitHub App](https://github.com/apps/garnet-runtime-review)'s, posted under its own installation (changed in v2.3.0: earlier releases posted a fallback comment themselves and asked for `pull-requests: write`). A top-level `permissions: {}` with per-job grants keeps each grant scoped to the job that needs it, rather than handing the whole workflow the default token scope. Fork pull requests are a separate mechanism: on the standard `pull_request` trigger, they receive no repository secrets and no `id-token: write`.

These are the workflow's grants to the action step. The [GitHub App](https://github.com/apps/garnet-runtime-review) below is a separate install with its own permission set; installing it changes nothing in your workflow file. The table below is what the App asks for; what an installation actually granted is visible only in the installing organization's settings. Recording does not depend on the App: the workflow grants above cover the action, the job summary and the profile in the dashboard on their own. The comment does.

## GitHub App permissions

Installing [Garnet Runtime Review](https://github.com/apps/garnet-runtime-review/installations/select_target) shows GitHub's permission screen. This is what each grant is used for, read from the public App registration (last changed 14 September 2026) and the control plane. The App asks for four grants and nothing under Checks or Workflows.

| Permission                    | What Garnet uses it for                                                                                                                                                                                                                                                                          |
| :---------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Metadata, read                | Required for every GitHub App. Resolves the installation for a repository and reads whether the repository is public, the check that gates the [public run report](#what-becomes-public).                                                                                                        |
| Pull requests, read and write | Reads the pull request's comments to find the one Runtime Review comment, posts it, and updates it in place as jobs finish. Reads the pull request's head and merge commits so a profile recorded on `refs/pull/N/merge` lands on the right commit. Nothing else on the pull request is written. |
| Contents, read                | One call, `GET /repos/{owner}/{repo}/commits/{sha}`, to read a commit's parents: it maps the merge commit back to the pull request head and finds the previous commit for comparison. GitHub files this endpoint under Contents. Garnet reads no file from the repository.                       |
| Actions, read                 | The grant GitHub requires for the `workflow_run` event below, which tells the App when a run's jobs finish so the comment updates in place. Garnet makes no Actions API call.                                                                                                                    |

### Webhook events

The App subscribes to `workflow_run` and `repository`. GitHub sends every App `installation` and `installation_repositories` as well. Deliveries go to `api.garnet.ai`, are checked against the webhook secret (`X-Hub-Signature-256`), queued, and deduplicated by delivery id.

| Event                                                                          | What Garnet does with it                                                                                                                                                                                                 |
| :----------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `workflow_run`, action `completed`                                             | Ends recording for a run whose runner was cancelled or died before the sensor reported its own stop. Without it the comment for that run would wait for a profile forever. Every other `workflow_run` action is ignored. |
| `repository` (`renamed`, `publicized`, `privatized`, `transferred`, `deleted`) | Keeps the repository's visibility current, so a public run report stops resolving the moment a repository goes private or is deleted.                                                                                    |
| `installation`, `installation_repositories`                                    | Records which repositories the installation covers, and drops them when the App is removed.                                                                                                                              |

The App does not subscribe to `pull_request` or `workflow_job`.

A webhook payload names the repository and its visibility, the run, its conclusion and its head commit. It carries no code, no diff and no log.

One change is planned: when the App starts posting a Check Run for each recorded job (informational only, it never fails a check), it will ask for Checks, write, and GitHub will ask you to approve that one new grant.

### Installing the App on a repository that runs the action

The App owns the Runtime Review comment; the action records. The App does not change what runs on the runner, what the sensor sends, or which jobs are recorded. Fork pull requests stay unrecorded with or without the App: the workflow there has no credentials, so nothing is recorded and the comment carries no fold for that job.

## Secrets

The action declares two token inputs, `api_token` and `github_token`, and reads no other entry in `secrets`.

* **Where `api_token` goes.** The action writes it to `/etc/default/jibril`, owned by root, mode `600`, and the sensor reads it from there. It is not passed on a command line and the run log prints the file redacted.
* **What it is used for.** Minting an Execution Profile into your Garnet organization is the use this path exercises; the token's full authorized scope is defined by Garnet, not by this workflow. Rotate it under **Settings → API Tokens** in [app.garnet.ai](https://app.garnet.ai/?utm_source=docs.garnet.ai\&utm_medium=web\&utm_content=security-token).
* **`github_token`** defaults to `${{ github.token }}`. It is used by `gh attestation verify` on the sensor release and to read the job's status when no Execution Profile was produced. It is not used to post anything.
* **Fork pull requests** receive no repository secrets and no `id-token: write`, so the action has no credential. It skips recording with a warning that names the missing credential, writes the same explanation to the job summary, and the job runs unrecorded. Nothing to configure.

<Note>
  **GitHub OIDC removes the secret.** Leave `api_token` empty and grant `id-token: write`: the action requests a short-lived GitHub OIDC token in the job and exchanges it with Garnet. A non-empty `api_token` always wins and no OIDC token is requested. Both paths are in the [quickstart](/quickstart#add-the-action-to-your-workflow).
</Note>

## What runs on the runner

The step downloads the Jibril sensor from [garnet-org/jibril-releases](https://github.com/garnet-org/jibril-releases/releases) over HTTPS, installs it with `sudo`, and starts it as a systemd service. The sensor runs as root because attaching eBPF programs requires it. Every eBPF program passes the kernel verifier before it loads.

* **Pin the sensor.** `jibril_version` defaults to `v2.17.0`; set it explicitly so the sensor version is visible in the workflow file. An empty value on `latest` lets the binary change under an unchanged action pin.
* **Integrity.** The download is the release bundle at the tag you pin, fetched over TLS from GitHub. Before the sensor runs, the action verifies the bundle's checksums and checks its signature with `gh attestation verify` against `garnet-org/jibril-releases`. If `github_token` is unset or the `gh` CLI is unavailable, attestation verification is skipped with a warning in the log.
* **Observe-only.** The sensor reads kernel events. It runs no sidecar, no proxy, and reorders none of your steps. Errors the action catches are logged as warnings and the job continues; that is error handling in the action, not a guarantee about every possible sensor or runtime failure.
* **Ephemeral.** The post step stops the sensor and removes its configuration and credentials from the runner before the job ends. The stop waits for the sensor to flush its Execution Profile, bounded by `stop_timeout_seconds` (default `1800`); on busy jobs this has been observed at 1–3 minutes.
* **Where it runs.** Linux x86\_64 runners with systemd and `sudo`. On any other platform or architecture the step logs a warning and skips. Runner by runner: [Coverage](/coverage).

Pin the action itself to a full commit SHA with a version comment, the shape Dependabot and Renovate keep current:

```yaml theme={null}
uses: garnet-org/action@245ad6be82de3200c205109c8ca7ac816dc692ea # v2.3.0
```

## What leaves the runner

The sensor sends one Execution Profile per job to `api.garnet.ai` over HTTPS. It carries:

* execution chains: process names, the recorded workflow step, and the working directory a process ran from;
* destinations: domains, IP addresses, ports, and protocols each outbound connection reached;
* runner facts: OS, architecture, kernel version, the workflow and job names, the run ID, and the commit.

From Jibril v2.15.0 on, the sensor redacts command arguments beyond the program name. The fields above are what the recorded profile is built from. It is not a copy of your repository, your environment, or your files.

The comment and the [public run report](/execution-profile#the-public-run-report) are two views of that record, not the record itself: the rendered comment leaves out fields the public JSON carries, process IDs among them. Read the JSON when you want the full recorded shape.

## What becomes public

The dashboard exposes recorded jobs to your Garnet organization. A Runtime Review comment is visible to whoever can read its pull request; on a public repository, that audience is public. A public run report is a separately published view. Confirm publication controls for your repository in the Garnet dashboard.

A published public run report resolves anonymously at its exact `?profile=` permalink. A bare run URL or a wrong selector fails closed with a 404. Anyone with a published report's URL can read it without signing in; the selector identifies the profile and is not authentication. Details: [The public run report](/execution-profile#the-public-run-report).

## Compliance

Current audit scope, reports, controls, subprocessors, and security documentation live in the [Trust Center](https://app.vanta.com/garnet.ai/trust).
