What you will build

  • A GitHub Actions workflow that triggers on pull requests.
  • An Autohand agent that reviews changed files.
  • A script that posts the review back to GitHub.

Prerequisites

  • A GitHub repository with Actions enabled.
  • An Autohand API key stored as AUTOHAND_API_KEY.
  • A GitHub token with pull request comment permissions.

Step 1: create the workflow

GitHub-hosted runners have no stored Autohand sign-in, so the workflow runs Autohand with --bare and sets AUTOHAND_PROVIDER, AUTOHAND_API_KEY, and AUTOHAND_AI_API_KEY. Without --bare, the CLI waits for a browser sign-in and the job hangs. See Authenticate in CI and containers.

name: Autohand Review
on:
  pull_request:
    types: [opened, synchronize]

jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Install Autohand
        run: curl -fsSL https://autohand.ai/install.sh | bash
      - name: Run review
        env:
          AUTOHAND_PROVIDER: autohandai
          AUTOHAND_API_KEY: ${{ secrets.AUTOHAND_API_KEY }}
          AUTOHAND_AI_API_KEY: ${{ secrets.AUTOHAND_API_KEY }}
        run: |
          autohand --bare -p             "Review the diff in this pull request. Focus on bugs, security, and clarity."             --restricted --json local > review.json
      - name: Post comment
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: node scripts/post-review.js review.json

Step 2: post the review

With --json local, review.json holds one object: {"type":"result","content":"..."} on success, or {"type":"error","message":"..."} on failure. The post step reads content and creates a PR comment. Use the language already available in your CI runner, or call the GitHub API directly with curl.

GitHub Actions

name: Autohand Review
on:
  pull_request:
    types: [opened, synchronize]

jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: curl -fsSL https://autohand.ai/install.sh | sh
      - run: autohand --bare -p "Review this pull request" --restricted --json local > review.json
        env:
          AUTOHAND_PROVIDER: autohandai
          AUTOHAND_API_KEY: ${{ secrets.AUTOHAND_API_KEY }}
          AUTOHAND_AI_API_KEY: ${{ secrets.AUTOHAND_API_KEY }}

JavaScript

import fs from 'node:fs/promises';
import { Octokit } from '@octokit/rest';

const review = JSON.parse(await fs.readFile(process.argv[2], 'utf8'));
const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/');

await new Octokit({ auth: process.env.GITHUB_TOKEN }).rest.issues.createComment({
  owner,
  repo,
  issue_number: Number(process.env.PR_NUMBER),
  body: review.content
});

TypeScript

import fs from 'node:fs/promises';
import { Octokit } from '@octokit/rest';

type Review = { type: string; content: string };

const review = JSON.parse(await fs.readFile(process.argv[2], 'utf8')) as Review;
const [owner, repo] = process.env.GITHUB_REPOSITORY!.split('/');

await new Octokit({ auth: process.env.GITHUB_TOKEN }).rest.issues.createComment({
  owner,
  repo,
  issue_number: Number(process.env.PR_NUMBER),
  body: review.content
});

Python

import json
import os
import requests
import sys

owner, repo = os.environ["GITHUB_REPOSITORY"].split("/")
review = json.load(open(sys.argv[1]))

requests.post(
    f"https://api.github.com/repos/{owner}/{repo}/issues/{os.environ['PR_NUMBER']}/comments",
    headers={"Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}"},
    json={"body": review["content"]},
)

Go

body, _ := os.ReadFile(os.Args[1])
var review struct{ Content string `json:"content"` }
json.Unmarshal(body, &review)

client := github.NewTokenClient(ctx, os.Getenv("GITHUB_TOKEN"))
owner, repo := splitRepo(os.Getenv("GITHUB_REPOSITORY"))
client.Issues.CreateComment(ctx, owner, repo, prNumber(), &github.IssueComment{
  Body: github.String(review.Content),
})

Java

String review = Files.readString(Path.of(args[0]));
String body = Json.parse(review).getString("content");
String[] repo = System.getenv("GITHUB_REPOSITORY").split("/");

github.createIssueComment(
  repo[0],
  repo[1],
  Integer.parseInt(System.getenv("PR_NUMBER")),
  body
);

Swift

let review = try JSONDecoder().decode(Review.self, from: Data(contentsOf: URL(fileURLWithPath: CommandLine.arguments[1])))
let repository = ProcessInfo.processInfo.environment["GITHUB_REPOSITORY"]!.split(separator: "/")

try await github.createIssueComment(
    owner: String(repository[0]),
    repo: String(repository[1]),
    number: Int(ProcessInfo.processInfo.environment["PR_NUMBER"]!)!,
    body: review.content
)

curl

curl -X POST \
  -H "Authorization: Bearer $GITHUB_TOKEN" \
  -H "Accept: application/vnd.github+json" \
  "https://api.github.com/repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \
  -d "$(jq -n --arg body "$(jq -r .content review.json)" '{body:$body}')" 

Step 3: tune the prompt

Iterate on the prompt to match your team's review style. Include context such as coding standards, test expectations, and areas to ignore.

Next steps

Add a check that fails the build when the agent finds blocking issues, or extend the bot to suggest code changes.