# Bring your own agent

Instead of a model key, a coding agent you already pay for can write the
fixes: **Claude Code**, **Codex**, **OpenCode**, **Hermes Agent** or **OpenClaw**. Upseam still finds the change, matches it
to your code, asks for approval, checks the result with its own gates and
opens the pull request. Your agent runs in your GitHub Actions with your
credentials, and Upseam never sees them.

> **Status.** This mode is new. The reusable workflow below is built from the
> official documentation of each action but has not yet been run end to end
> on GitHub Actions, and it needs the first release of `upseam/action`. Try it
> on a test repository first.

## Set it up

1. Name the agent in `.github/upseam.yml` on the default branch:

   ```yaml
   agent: claude-code
   ```

   `codex`, `opencode`, `hermes` and `openclaw` work the same way. `agent: claude-code` is short for
   `agent: { runner: claude-code }`; both forms mean the same. The default
   `upseam` keeps the model from the settings page (see
   [Connect a model](models.md)). With any other value, this
   repository uses its own agent even when no model is connected.

2. Add the short workflow for your agent as
   `.github/workflows/upseam-agent.yml` on the default branch: the same
   dozen lines for [Claude Code](#claude-code), [Codex](#codex),
   [OpenCode](#opencode), [Hermes Agent](#hermes-agent) and
   [OpenClaw](#openclaw), only the runner, the model and the secret differ.
   Add your agent's credential as an Actions secret. Until a workflow there
   listens for `repository_dispatch` with the type `upseam-agent`, Upseam
   sends nothing and says what to add in the dashboard issue and in Slack.

3. Pin the [reusable workflow](#the-reusable-workflow) to the full commit
   SHA of an `upseam/action` release:
   `git ls-remote https://github.com/upseam/action 'refs/tags/<tag>' 'refs/tags/<tag>^{}'`.
   If a `^{}` line is printed, use its SHA — the tag is annotated and that
   line is its commit — otherwise the plain line already is the commit.

## What Upseam sends

After approval (the default [ask mode](delivery-modes.md)) Upseam sends one
`repository_dispatch` event with the type `upseam-agent`. It carries the group
key, the commit to start from, the branch head Upseam expects, the id of this
dispatch, the runner, the files the agent may edit, and a task for the agent:
rules, those files, the matched places and the change data. The task has no
file contents, no key and no token. The vendor's change text in it is marked
as untrusted.

## How an edit becomes a pull request

Your CI and other systems that build branches run `upseam/*` branches with
their secrets before anyone reviews them, and workflow artifacts of a public
repository can be downloaded by anyone signed in to GitHub. So nothing your
agent writes leaves its job or reaches a branch before Upseam's checks pass:

1. The agent job can only read the repository. It runs your agent.
2. In the same job, a fresh checkout receives only the listed files, and
   `upseam/action` checks them: only edits of existing files, and the same
   [patch gates](security.md#patch-gates) as for a model's patch. A secret
   written into a file is a new literal, so the job stops. Only if the check
   passes are the files copied out of the workspace and uploaded.
3. The `push` job runs no agent and holds no agent credential. It checks the
   files again and pushes one commit to `upseam/<group>`, marked with the id
   of the dispatch.
4. Upseam checks the push once more. If anything fails, the finding moves to
   **Needs you** with the reason, no pull request opens, and Upseam moves the
   branch back to where it was. A push from an older dispatch is dropped the
   same way.

If nothing is pushed within an hour, the finding moves to **Needs you** with a
hint to look at the `upseam-agent` workflow runs. The pull request says your
agent wrote it and Upseam checked it.

## The reusable workflow

Every agent uses the same reusable workflow,
`upseam/action/.github/workflows/agent.yml`. Your file names it, pinned to a
full commit SHA, and sets four things:

- `runner`: `claude-code`, `codex`, `opencode`, `hermes` or `openclaw`, the
  same value as `agent` in `.github/upseam.yml`;
- `model`: `provider/model id` for OpenCode, Hermes Agent and OpenClaw, left
  out for Claude Code and Codex;
- `model-key`: the only secret the workflow receives, your agent's token or
  your model provider's API key;
- `permissions: contents: write`: the most the workflow may use. It gives
  the agent only read access and write access only to the job that pushes.

The workflow first checks that the event is `upseam-agent`, that `runner`
is one of the five and matches what Upseam sent, and that `model` names a
provider the runner supports (`anthropic`, `openai` for OpenCode only, or
`openrouter`); anything else stops the run before an agent starts. Then it
runs your agent's job, the check in a fresh checkout and the upload, and
finally the job that pushes, as described above. Every action in it is
pinned to a commit, every image to a digest and every download to a hash. It
never uses `secrets: inherit`.

You trust the workflow file at the SHA you pin instead of steps copied into
your repository: read it at that SHA before pinning, and it changes only when
you change the SHA. Don't pin a tag or a branch.

## Claude Code

Claude Code runs through
[anthropics/claude-code-action](https://github.com/anthropics/claude-code-action).
Create the secret `CLAUDE_CODE_OAUTH_TOKEN` with `claude setup-token`, or
pass an Anthropic API key as `model-key` instead; a key that starts with
`sk-ant-oat` goes to the action as a sign-in token, any other as an API key.

```yaml
name: upseam-agent
on:
  repository_dispatch:
    types: [upseam-agent]
jobs:
  agent:
    uses: upseam/action/.github/workflows/agent.yml@<40-character SHA> # v0.x, after the first release
    permissions:
      contents: write
    with:
      runner: claude-code
    secrets:
      model-key: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
```

- The agent gets only the `Read`, `Edit`, `Glob` and `Grep` tools. The
  `settings` block stops reads outside the repository and of `/proc`, where
  the agent's own environment lives, and edits of `.git`, workflow files and
  the runner's action and temporary directories. The paths assume
  GitHub-hosted Ubuntu runners.
- `github_token: ${{ github.token }}` keeps the action on the read-only
  workflow token, so the Claude GitHub App is not needed.
- `allowed_bots: ${{ github.actor }}` lets the Upseam App's bot start the
  action. Only accounts with write access can send the event at all.

## Codex

[openai/codex-action](https://github.com/openai/codex-action) takes an OpenAI
API key (secret `OPENAI_API_KEY`).

```yaml
name: upseam-agent
on:
  repository_dispatch:
    types: [upseam-agent]
jobs:
  agent:
    uses: upseam/action/.github/workflows/agent.yml@<40-character SHA> # v0.x, after the first release
    permissions:
      contents: write
    with:
      runner: codex
    secrets:
      model-key: ${{ secrets.OPENAI_API_KEY }}
```

The workflow runs Codex in the `:workspace` permission profile, which limits
where Codex writes, and the action removes sudo before Codex runs by default. Upseam has not checked
which paths outside the workspace this profile lets Codex read.

## OpenCode

[OpenCode](https://github.com/anomalyco/opencode) has no official GitHub
Action for this, so the workflow runs the pinned CLI itself. It takes the key
of the model provider you choose: Anthropic (`ANTHROPIC_API_KEY`), OpenAI
(`OPENAI_API_KEY`) or OpenRouter (`OPENROUTER_API_KEY`), whose SDKs are built
into OpenCode; set `model` to `provider/model id`. The workflow turns off
OpenCode's built-in plugins, so their sign-in (OAuth or CLI) methods are
unavailable, for example GitHub Copilot or ChatGPT sign-in for Codex.

```yaml
name: upseam-agent
on:
  repository_dispatch:
    types: [upseam-agent]
jobs:
  agent:
    uses: upseam/action/.github/workflows/agent.yml@<40-character SHA> # v0.x, after the first release
    permissions:
      contents: write
    with:
      runner: opencode
      model: anthropic/<model id>
    secrets:
      model-key: ${{ secrets.ANTHROPIC_API_KEY }}
```

- The job downloads OpenCode 1.18.32 for Linux x64 from npm and checks a
  pinned SHA-512 before unpacking it; no install script runs. Upseam checked
  that this binary is identical to the one in OpenCode's attested GitHub
  release. It also installs ripgrep 15.1.0, which OpenCode's search tools
  use, after checking its SHA-256, and marks OpenCode's plugin package as
  installed, so OpenCode itself downloads neither at startup.
- The profile in `OPENCODE_CONFIG_CONTENT` denies every tool first, then
  allows only reading, searching and editing files in the repository. There
  is no shell, no web access, no subagents and no access outside the
  repository, including OpenCode's own tool-output directory. The agent
  cannot open `.env` files or `.git`, or edit workflow files or OpenCode's
  config. Its search tool does not apply these file rules, so it can show
  lines of a `.env` file that is committed to the repository; anyone who can
  read the repository can already see such a file.
- Without `--auto`, a headless run rejects anything the profile does not
  allow. The workflow passes none of `--auto`, `--yolo` or
  `--dangerously-skip-permissions`.
- Before the run, the step deletes any OpenCode config and symbolic links in
  the checkout, so the repository cannot add tools, plugins or MCP servers
  or point a file outside it. The task reaches OpenCode on stdin only.
- Upseam checked these settings against the OpenCode docs and the source of
  this version but has not run this job end to end yet, and has not checked
  which model ids it accepts with the remote model list turned off.

### Remaining risk (Claude Code, Codex, OpenCode)

- The agent job holds your agent's credential while the agent reads vendor
  text that could try to steer it. The tool limits and read rules above are
  what keep the agent from reaching it.
- In a public repository, the uploaded files and the run logs are public. Only
  files that passed the check are uploaded, so they hold nothing that would
  not be in the pull request.

## Hermes Agent

[Hermes Agent](https://github.com/NousResearch/hermes-agent) cannot be
limited with its own settings the way Claude Code is: its file tools read any
path its user can, including its own environment in `/proc/self/environ`,
and its write guard is "not a hard boundary" by its own documentation. So
Hermes runs inside a Docker container, and the container is the boundary.
Create the Actions secret `ANTHROPIC_API_KEY` with an Anthropic API key.

```yaml
name: upseam-agent
on:
  repository_dispatch:
    types: [upseam-agent]
jobs:
  agent:
    uses: upseam/action/.github/workflows/agent.yml@<40-character SHA> # v0.x, after the first release
    permissions:
      contents: write
    with:
      runner: hermes
      model: anthropic/claude-sonnet-4-6
    secrets:
      model-key: ${{ secrets.ANTHROPIC_API_KEY }}
```

For OpenRouter, set `model` to `openrouter/` and an OpenRouter model id, such
as `openrouter/anthropic/claude-sonnet-4.6`, and pass `OPENROUTER_API_KEY` as
`model-key`.

### How the sandbox works

- **Only the listed files.** The job copies the files Upseam listed into a
  separate directory and mounts only that directory, at `/work`. The
  container sees no checkout, no `.git`, no `.github`, no runner home or
  temporary directory and no Docker socket.
- **A locked-down container.** The root filesystem is read-only, `/tmp` is
  an in-memory scratch space, and Hermes runs as the unprivileged user
  `65534` with every Linux capability dropped, `no-new-privileges` and
  limits on memory (swap included) and processes.
- **Only the model key.** The container gets the model key and a few Hermes
  settings, nothing else: no `GITHUB_TOKEN`, no `GH_TOKEN`, no Actions
  runtime token. What Hermes can read in `/proc/self/environ` is the key you
  already gave the model.
- **Only the model's host.** The container sits on an internal Docker network
  with no route out and no address for the runner itself (Docker 28 or
  later, as on GitHub-hosted runners). Its one way out is a small proxy,
  shown in full in the reusable workflow, that only opens connections to `PROVIDER_HOST`
  on port 443. It logs the host of each allowed connection and a bare `deny`
  for each refused one, never what the client sent.
- **Only file tools.** `-t file` gives Hermes read, write, patch and search.
  `--safe-mode` turns off plugins, MCP servers, hooks, user config and
  `AGENTS.md` injection; the environment turns off the tirith download and
  runtime package installs. Hermes sends no telemetry by default, and its
  update check has no git checkout to look at.
- **Checked like any other agent.** After the container exits, only regular
  files at their listed paths, reached without symlinks, are copied back into a checkout the container
  never saw, and `upseam/action` checks them with the same
  [patch gates](security.md#patch-gates) before anything is uploaded or
  pushed.

Hermes is built from source at a fixed commit, `f97608f` (tag `v2026.9.24`,
package version 0.21.5), checked with `git rev-parse`; its Python packages
come from its own `uv.lock`, which fixes every file by hash, and only as
prebuilt wheels. The base image is the uv image pinned by digest, the one
Hermes' own Dockerfile uses as a build stage. Nous Research does not sign its release tags, so the commit hash
is the only link to what they published.

### Remaining risk (Hermes Agent)

- Hermes can read the model key. The provider already has it; a listed file
  that contains it is a new literal the gates reject; Hermes' output and
  error output are not printed, only its exit code, since a reshaped copy
  would get past GitHub's log masking; and the proxy log holds no client
  data. What stays visible is whether the job passed, the exit code, how
  long it ran and how many connections the proxy allowed.
- Hermes can fill the runner's disk by writing into `/work`; the job then
  fails, and nothing leaves it.
- Vendor text can still steer the edits. The patch gates decide what reaches
  a branch.
- Hermes sends the task and the files it reads to the model provider, as
  any agent does. It has no web or shell tool of its own.
- The job needs a GitHub-hosted Ubuntu runner with Docker Engine 28 or later
  (for the isolated gateway). On Docker older than 26, an internal network
  may still send DNS queries out through the runner's resolver. Self-hosted
  runners are not supported: jobs sharing one Docker daemon would collide on
  the fixed container and network names, one job's cleanup could remove
  another's proxy, and a cancelled job could leave its agent container
  running.
- The workflow is built from Hermes source and Docker documentation and has
  not yet been run on GitHub Actions.

## OpenClaw

[OpenClaw](https://github.com/openclaw/openclaw) runs in the same sandbox as
Hermes Agent. Its headless `openclaw agent exec` turns on the shell by
default, so the job also denies every tool except the five file tools in its
config; the container is still the boundary. Create the Actions secret
`ANTHROPIC_API_KEY`.

```yaml
name: upseam-agent
on:
  repository_dispatch:
    types: [upseam-agent]
jobs:
  agent:
    uses: upseam/action/.github/workflows/agent.yml@<40-character SHA> # v0.x, after the first release
    permissions:
      contents: write
    with:
      runner: openclaw
      model: anthropic/claude-sonnet-4-6
    secrets:
      model-key: ${{ secrets.ANTHROPIC_API_KEY }}
```

For OpenRouter, set `model` to an `openrouter/...` model id and pass
`OPENROUTER_API_KEY` as `model-key`.

### How the OpenClaw sandbox differs

- The container, the mount, the environment and the proxy are the same as
  for [Hermes Agent](#how-the-sandbox-works). The proxy runs from its Python
  image, OpenClaw from a Node image; both are pinned by digest.
- OpenClaw gets only `ls`, `read`, `write`, `edit` and `apply_patch`, limited
  to the workspace. The shell, web, browser, sessions, memory, messaging,
  plugins and MCP tool groups are denied, the shell mode is `deny`, Code
  Mode is off, and skills, `AGENTS.md`-style context files and the update
  check are off.
- OpenClaw is installed from npm at a fixed version whose package hash the
  build compares with the one in the job. Its dependencies are resolved as
  of the day of that release, and npm checks each against the registry's
  hash. The package's npm provenance names build commit `a6e3d5d`, not the
  commit the `v2026.9.6` tag points to; the job does not check provenance.
- The job runs OpenClaw with a pinned config file. The model key reaches it
  only through the environment: the run starts with an empty state
  directory and an empty home, so there are no stored credentials to find.

### Remaining risk (OpenClaw)

- The same as for Hermes Agent: where the model key can go, disk use in
  `/work`, vendor text steering the edits, the runner requirements (Docker 28,
  DNS on Docker older than 26, no self-hosted runners), and the workflow has
  not yet been run on GitHub Actions.
- Upseam has not yet confirmed on a run that OpenClaw starts on a read-only
  root, that the final tool list is exactly the five file tools, and that the
  npm package matches its build commit.

## Terms

- **Anthropic** documents `claude setup-token` and `CLAUDE_CODE_OAUTH_TOKEN`
  for GitHub Actions. Its
  [legal and compliance page](https://code.claude.com/docs/en/legal-and-compliance)
  says subscription sign-in is meant for ordinary use of Claude Code. Upseam
  never receives or routes your token, but whether a workflow that Upseam
  triggers counts as ordinary use is not stated. Use an API key if in doubt;
  Anthropic also recommends one for a secret shared across an organization.
- **OpenAI** recommends API keys for Codex in CI, and the Codex workflow uses
  one.
- **OpenCode** is open source (MIT) and uses the model provider's key you
  give it, so that provider's terms apply. The OpenCode job uses an API key.
- **Hermes Agent** is MIT-licensed and sends requests with the key you give
  it, so the model provider's terms apply. Use an API key: Anthropic's page
  above reserves subscription sign-in for Claude Code and other native
  Anthropic applications.
- **OpenClaw** is MIT-licensed and, like Hermes Agent, calls the provider
  with the key you give it; the workflow expects an API key.
