View as markdown

GitHub access

A step gets GitHub authority from a github block:

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: |
          <user-prompt>
          ${{ paseo.prompt }}
          </user-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

FieldNotes
connectionThe project's GitHub connection slug.
repositoriesThe repositories the token can reach. On a GitHub-triggered run, this defaults to the triggering repository. Required for other triggers.
permissionsGitHub installation-token permission names, such as contents, pull_requests, issues. Defaults to contents: read.
durationHow 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:

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: |
              <user-prompt>
              ${{ paseo.prompt }}
              </user-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: |
              <user-prompt>
              ${{ paseo.prompt }}
              </user-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: |
              <user-prompt>
              ${{ paseo.prompt }}
              </user-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 covers the rest of the authority model.

Other integrations

Any connection value can be supplied explicitly through a step's environment:

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.