---
title: GitHub access
description: Grant a workflow step a scoped GitHub token and a working git setup, minted per execution and revoked when the step ends.
nav: GitHub access
order: 65
category: Hub
---
# GitHub access
A step gets GitHub authority from a `github` block:
```yaml
steps:
- id: implement
environment: development
max_runtime: 90m
idle_timeout: 10m
github:
connection: example-github
repositories:
- example/project
permissions:
contents: write
pull_requests: write
agent:
provider: codex
mode: full-access
prompt:
- text: |
Implement the request, commit, push the branch, and open a
pull request with `gh pr create`.
Call hub.finish_execution when the step is complete.
- text: |
${{ paseo.prompt }}
```
The agent can clone, commit, push, and call `gh` in the listed repositories. Nothing is configured on the daemon host.
Nothing else grants GitHub authority. A run triggered from a GitHub event has no token and no git configuration unless its step declares the block.
## The block
| Field | Notes |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `connection` | The project's GitHub connection slug. |
| `repositories` | The repositories the token can reach. On a GitHub-triggered run, this defaults to the triggering repository. Required for other triggers. |
| `permissions` | GitHub installation-token permission names, such as `contents`, `pull_requests`, `issues`. Defaults to `contents: read`. |
| `duration` | How long the token lives. Defaults to `1h`, the GitHub maximum. Shorter revokes early; longer is invalid. |
Permissions cannot exceed what the GitHub App installation was granted. A block asking for `contents: write` on an installation that only has read fails.
Hub mints the token when the step starts and revokes it when the execution ends, whatever the outcome.
## What the agent's environment contains
Hub supplies `GH_TOKEN` and git configuration through environment variables alone:
- Commits author as the App's bot account, with its GitHub noreply email.
- `git@github.com` remote URLs are rewritten to HTTPS, and the credential helper answers with the token.
- Global and system git config are ignored, and nothing prompts a terminal.
The daemon host's own git identity and credentials are never used or changed.
## Only the step that needs it
Authority is per step, so a routed workflow grants it only to the branch that does the work. A Slack-triggered run:
```yaml
environments:
- name: triage
kind: daemon
daemon: my-macbook
cwd: /Users/you/code/project
- name: development
kind: daemon
daemon: my-macbook
cwd: /Users/you/code/project
worktree:
mode: branch-off
newBranch: hub/work
base: origin/main
triggers:
- name: slack-work
on: slack.mention
max_runtime: 3h
filters:
workspace: T01234567
channels: [C01234567]
from_users: [U01234567]
steps:
- id: classify
environment: triage
max_runtime: 2m
idle_timeout: 30s
agent:
provider: codex
options:
approval_policy: never
sandbox_mode: read-only
web_search: disabled
prompt:
- text: |
Decide whether this request needs an answer or a change.
Call hub.finish_execution with the decision as the structured result.
- text: |
${{ paseo.prompt }}
output:
schema:
type: object
additionalProperties: false
required: [kind]
properties:
kind:
enum: [answer, change]
- id: answer
if: ${{ steps.classify.outputs.kind == 'answer' }}
environment: triage
max_runtime: 10m
idle_timeout: 2m
agent:
provider: codex
options:
approval_policy: never
sandbox_mode: read-only
web_search: disabled
prompt:
- text: |
Answer the request without changing files.
Call hub.reply to send your answer to the originating conversation.
Call hub.finish_execution when the step is complete.
- text: |
${{ paseo.prompt }}
allow_outputs:
- type: slack.reply
- id: change
if: ${{ steps.classify.outputs.kind == 'change' }}
environment: development
max_runtime: 90m
idle_timeout: 10m
github:
connection: example-github
repositories:
- example/project
permissions:
contents: write
pull_requests: write
agent:
provider: codex
mode: full-access
prompt:
- text: |
Make the change, verify it, commit, push the branch, and open a
pull request with `gh pr create`.
Call hub.reply with a link to the pull request.
Call hub.finish_execution when the step is complete.
- text: |
${{ paseo.prompt }}
allow_outputs:
- type: slack.reply
```
The classifier and the answer step read untrusted text with no GitHub authority. Only the selected worker holds a token, scoped to one repository. [Hub security](/docs/hub/security) covers the rest of the authority model.
## Other integrations
Any connection value can be supplied explicitly through a step's environment:
```yaml
steps:
- id: use-connection
env:
SOME_TOKEN: "${{ paseo.connections.some-connection.token }}"
```
Hub resolves the value when it launches the step and does not persist it. The `github` block adds repository and permission restrictions plus the git setup needed for GitHub work.