---
title: Review Pull Requests Automatically (public)
description: Fire a Harness Runtime session from a GitHub webhook so every pull request gets an unattended first-pass review delivered to Slack.
product: Managed Agents
url: https://docs.digitalocean.com/products/managed-agents/agent-harness-runtime/examples/review-pull-requests/
last_updated: "2026-09-22"
---

> **For AI agents:** The documentation index is at [https://docs.digitalocean.com/llms.txt](https://docs.digitalocean.com/llms.txt). Markdown versions of pages use the same URL with `index.html.md` in place of the HTML page (for example, append `index.html.md` to the directory path instead of opening the HTML document).

# Review Pull Requests Automatically (public)

DigitalOcean Harness Runtime provides managed, hardware-isolated microVM sandboxes with built-in tools such as Chromium to run harnesses and execute arbitrary code. Rich lifecycle APIs preserve conversational history and working state across sessions, letting you pause, resume, and fork work to control costs and adapt to the nonlinear nature of agentic workflows. Scale complete agents such as Claude Code or use sandboxes independently for code execution, all through the same service. See [What You Can Build](https://docs.digitalocean.com/products/managed-agents/agent-harness-runtime/details/what-you-can-build/index.html.md) for example use cases.

A first-pass review catches the routine problems before a human spends attention on them. This example runs Claude Code from a GitHub webhook: a pull request opens, a session starts, the agent reads the diff and posts its notes to Slack, and the session is torn down.

The important difference from [Refactor a Repository](https://docs.digitalocean.com/products/managed-agents/agent-harness-runtime/examples/refactor-a-repository/index.html.md) is that nobody is at a terminal. That changes how permissions have to be written.

## Write a Spec That Can Run Unattended

There is no one to answer an approval prompt on a triggered run, so triggers reject a spec that could produce one. With `--session-mode fresh`, creating a trigger fails if the spec’s `permissions` default is `ask`, if any rule uses `action: ask`, or if there is no `permissions` block at all.

The pattern that works is an allow default with explicit denies for anything the agent must never do. Save the following as `pr-review.yaml`:

```yaml
name: pr-reviewer
agent: claude-code
size: mars-2vcpu-4gb
repos:
  - your-org/your-service
env:
  HARNESS_INFERENCE_MODEL: anthropic-claude-5-sonnet
secrets:
  HARNESS_INFERENCE_API_KEY: ${HARNESS_INFERENCE_API_KEY}
  GITHUB_TOKEN: ${GITHUB_TOKEN}
egress:
  - github.com
  - api.github.com
permissions:
  default: allow
  rules:
    - tool: file.write
      action: deny
    - tool: bash
      match: { command: "rm -rf *" }
      action: deny
```

Model access works the same as in [Refactor a Repository](https://docs.digitalocean.com/products/managed-agents/agent-harness-runtime/examples/refactor-a-repository/index.html.md): the [model access key](https://docs.digitalocean.com/products/inference/how-to/manage-model-access-keys/index.html.md) in `HARNESS_INFERENCE_API_KEY` is captured when the trigger is created and reused on every firing, so nothing has to supply a credential when a pull request opens.

`file.write` is denied because this agent’s job is to read and report. It clones the repository, reads the diff, and writes a summary as its final output. Nothing it does should change the checkout.

The choice of adapter matters for that deny. A permission rule naming a tool the adapter does not declare is rejected at session create rather than ignored, and the Codex CLI adapter declares only `bash`. This spec uses `claude-code`, which declares `file.write`, so the rule is enforceable. See [Agent Adapters](https://docs.digitalocean.com/products/managed-agents/agent-harness-runtime/concepts/agent-adapters/index.html.md) for what each adapter supports.

**Warning**:

  `ask` is not a safety control on an unattended run. If a prompt does occur at run time, the platform approves it automatically so the run can finish, and records it as auto-approved in the transcript. Anything the agent must never do needs a `deny` rule.

## Create the Trigger

Create a webhook trigger bound to that spec. `--provider github` tells the platform how to verify the signature on inbound deliveries:

```shell
doctl harness-runtime triggers create \
  --kind webhook \
  --name pr-reviewer \
  --session-mode fresh \
  --spec pr-review.yaml \
  --provider github \
  --prompt "Clone your-org/your-service and review this pull request: {{.pull_request.html_url}}. Report correctness risks, missing tests, and anything that looks unintentional. Be brief." \
  --output-mode slack \
  --output-slack-webhook <your-slack-incoming-webhook-url>
```

`--session-mode fresh` starts a new session per delivery and tears it down afterward, which is what you want when reviews are independent of each other.

The prompt can reach into the webhook payload. `{{.pull_request.html_url}}` pulls a single field out of the JSON body, and `{{payload}}` embeds the whole delivery. A field that is not present becomes an empty string.

The response includes the trigger’s webhook URL and secret. The secret is shown once, so copy both values and paste them into your repository’s webhook settings on GitHub before you move on. If you lose the secret, issue a new one with `doctl harness-runtime triggers rotate-secret`.

## Check That It Ran

Every firing is recorded as an execution:

```shell
doctl harness-runtime triggers list-executions <trigger-id>
doctl harness-runtime triggers get-execution <trigger-id> <execution-id>
```

`get-execution` shows the inbound payload alongside the agent’s output, which is usually enough to tell a bad prompt from a bad payload. Each execution also records its session ID, so you can replay the whole transcript:

```shell
doctl harness-runtime logs <session-id>
```

## Adapt It

| To do this | Change this |
|---|---|
| Run on a schedule instead of an event | Use `--kind cron` with `--cron-expr "0 9 * * 1"` and `--timezone`. Scheduled firings have no payload, so the prompt cannot use `{{payload}}` or `{{.field}}`. |
| Deliver somewhere other than Slack | Use `--output-mode email` with `--output-email`, or `none` if the run’s own side effects are the point. |
| Keep context between firings | Pause a session and bind it with `--session-mode reuse` and `--bound-session-id`. The agent keeps its workspace and history across firings. |
| Take a source other than GitHub | Use `--provider gitlab`, or `custom` for plain HMAC. Run `doctl harness-runtime triggers list-providers` for the current list. |
| Let the agent comment on the pull request | Give it GitHub tool access through [Action Gateway](https://docs.digitalocean.com/products/managed-agents/agent-harness-runtime/how-to/connect-action-gateway/index.html.md) and allow the specific tool in `permissions`. |
| Stop it temporarily | `doctl harness-runtime triggers pause <trigger-id>`. Resuming does not replay firings missed while paused. |

## Related

- [Run Agents With Triggers](https://docs.digitalocean.com/products/managed-agents/agent-harness-runtime/how-to/run-agents-with-triggers/index.html.md) for the full trigger surface.
- [Configure Permissions](https://docs.digitalocean.com/products/managed-agents/agent-harness-runtime/how-to/configure-permissions/index.html.md) for how rules are evaluated.
- [Approvals](https://docs.digitalocean.com/products/managed-agents/agent-harness-runtime/concepts/approvals/index.html.md) for what happens when an agent asks and nobody answers.