Dylan Andersen's DocsDylan Andersen's Docs
Claude Code + SalesforceExpert Claude Code

CI Reviews & Handoff Automation

Run Claude Code in GitHub Actions to review POC PRs, summarize metadata deltas between demos, and generate handoff docs on every tag. Zero-click artifacts for the customer.

The last SE mile of any POC is handoff. Customer's architect wants a metadata inventory. Customer's admin wants a release-notes doc. Customer's security team wants a permission diff. Internally, your delivery team wants a clean PR review before you even demo. Claude Code running in CI (GitHub Actions, GitLab CI, Bitbucket Pipelines) closes all of that without eating your Friday.

CI Reviews and Handoff Automation hero

Why CI and not just local

Local scripts (the Headless Automation page) work for you. CI works for everyone: the customer's fork, the internal delivery team, the archived POC branch three months later. Same prompts, reproducible environment, no "works on my machine".

The anatomy of a Claude Code GitHub Action

The setup is boring and short:

# .github/workflows/claude-pr-review.yml
name: Claude PR Review

on:
  pull_request:
    branches: [main]

jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # need full history for diff context

      - name: Install Claude Code
        run: |
          curl -fsSL https://claude.ai/install.sh | bash
          echo "$HOME/.local/bin" >> $GITHUB_PATH

      - name: Install sf CLI
        run: npm install -g @salesforce/cli

      - name: Run review
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: ./scripts/ci-review.sh ${{ github.event.pull_request.number }}

One secret (ANTHROPIC_API_KEY), one shell script, and you have a Claude-powered reviewer on every PR.

The review script

#!/usr/bin/env bash
# scripts/ci-review.sh
set -euo pipefail

PR_NUMBER="${1:?PR number required}"

# Get the diff
git diff --unified=5 origin/main...HEAD > /tmp/pr.diff

# Review it
REVIEW=$(claude -p "$(cat <<'EOF'
You are reviewing a pull request on a Salesforce POC repository.
The diff is in /tmp/pr.diff. Check for:

1. Profiles or permission sets that widen access inappropriately
2. Apex without test coverage
3. SOQL in loops or other governor-limit anti-patterns
4. Agent metadata (.genAiPlugin, .genAiPromptTemplate) changes that widen scope
5. Hardcoded IDs or credentials
6. Data 360 DLO/DMO changes that could break existing segments

Output as markdown. Each finding: severity (blocker/warning/nit), file:line, one-sentence description, one-sentence fix.
Return a short "Looks good" line if nothing found.
EOF
)" \
  --allowedTools "Read" \
  --dangerously-skip-permissions \
  --model sonnet)

# Post to the PR
gh pr comment "$PR_NUMBER" --body "## Claude review

$REVIEW"

Two things to notice. --allowedTools "Read" is the seatbelt: Claude can see files but cannot edit or run anything in CI. --dangerously-skip-permissions is fine because CI is ephemeral and has no sf auth to a real org.

Four ready-to-use Actions

Permission-diff reviewer

Fires on any PR that touches .profile-meta.xml or .permissionset-meta.xml:

on:
  pull_request:
    paths:
      - 'force-app/main/default/profiles/**'
      - 'force-app/main/default/permissionsets/**'

Script:

git diff origin/main...HEAD -- 'force-app/main/default/profiles/**' \
     'force-app/main/default/permissionsets/**' \
  | claude -p "$(cat <<'EOF'
Analyze this permission delta. For each change:
- Which profile/permset is affected
- What access is gained or lost
- Risk level for a customer POC about to move to production
Flag any 'Modify All' or 'View All' additions as blockers.
EOF
)" --allowedTools "Read"

Post as a PR comment. Customer security teams love this. Your delivery lead loves it more.

Agent metadata reviewer

Triggered when .genAiPlugin or .genAiPromptTemplate changes:

git diff origin/main...HEAD -- 'force-app/main/default/genAiPlugins/**' \
     'force-app/main/default/genAiPromptTemplates/**' \
  | claude -p "$(cat <<'EOF'
Review these Agentforce metadata changes. Check:
- Topic descriptions are customer-safe (no internal codenames)
- Prompt templates don't leak system instructions in user-visible output
- New actions have appropriate invocation guards
- Topic overlaps that could cause routing confusion
Output markdown. Flag blockers that would be visible in the customer demo.
EOF
)" --allowedTools "Read"

Release-notes generator on tag

Every time you tag a POC milestone (v0.3.0-demo2), generate release notes:

on:
  push:
    tags: ['v*']

jobs:
  release-notes:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - # install claude, sf...
      - run: |
          PREV=$(git describe --tags --abbrev=0 HEAD^)
          git log "$PREV..HEAD" --pretty=format:'%s (%an)' > /tmp/commits.txt
          claude -p "Convert these commits into customer-facing release notes. Group by theme (Agent, Data 360, UI, Integrations). Plain English, no jargon." \
            --allowedTools "Read" \
            < /tmp/commits.txt \
            > RELEASE_NOTES.md
          gh release create "${GITHUB_REF#refs/tags/}" RELEASE_NOTES.md --notes-file RELEASE_NOTES.md

POC handoff doc writes itself.

Nightly regression-sweep-to-Slack

Runs the Agentforce test suite against a shared UAT nightly and posts a triage to Slack. This one needs sf auth, so it uses a JWT connected app and encrypted secrets:

on:
  schedule:
    - cron: '0 13 * * 1-5'  # 6 AM PT weekdays

jobs:
  sweep:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - # install claude, sf
      - name: Auth to UAT
        env:
          SF_JWT_KEY: ${{ secrets.SF_JWT_KEY }}
        run: |
          echo "$SF_JWT_KEY" > /tmp/server.key
          sf org login jwt \
            --username poc-ci@acme.com.uat \
            --jwt-key-file /tmp/server.key \
            --client-id ${{ secrets.SF_CONSUMER_KEY }} \
            --alias acme-uat --set-default
      - name: Run sweep
        run: ./scripts/nightly-agent-sweep.sh
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}

See Connected Apps for the JWT flow setup.

Cost and runtime discipline

CI minutes and Claude turns both cost real money. Four rules:

Every CI job that calls claude -p should log the cost:

RESULT=$(claude -p "..." --output-format json)
COST=$(echo "$RESULT" | jq -r .total_cost_usd)
echo "::notice::Claude turn cost: \$$COST"

That shows up in the Actions summary. Check it weekly. A PR review costing $0.30 is fine. $3.00 means your prompt or input is too big.

paths: filters on your workflow are free cost discipline. Don't run the permission-diff reviewer on every PR, only on PRs that touch permission files.

Same with git diff inside the script. Pipe only the relevant paths, not the whole diff.

Reviews that find blockers should fail the check:

if echo "$REVIEW" | grep -q "blocker"; then
  echo "::error::Blockers found in Claude review"
  exit 1
fi

Don't let the CI check go green while the comment says "blocker". The merge button shouldn't be available.

If your prompt references large fixed context (e.g., an architecture doc), put it in the repo and reference it in the prompt as a file. Claude Code reads the file once per turn. Caching the setup steps (uses: actions/cache@v4 for ~/.local/bin) also saves 30+ seconds per run.

Secrets hygiene

Three ground rules for Claude-Code-in-CI:

  1. ANTHROPIC_API_KEY in Actions secrets only. Never in the repo. Never in a public Gist. Rotate quarterly.
  2. sf JWT keys in Actions secrets, never in plaintext. Write the key to /tmp at runtime, and the runner is ephemeral.
  3. Limit --allowedTools ruthlessly. CI almost always only needs Read. No Edit, no Bash, no Write unless the job's entire purpose is to write one file and commit it.

Never attach production org auth to CI

CI jobs are a fine target for automation, but they should only ever reach scratch orgs, UAT sandboxes, or read-only integrations. Never give CI a way to write to customer production. Use an environment variable scoped to a non-prod environment, gated by environment: rules that require manual approval for anything production-shaped.

Full PR review + auto-fix flow

The most advanced shape: Claude reviews the PR, and on minor findings (nits), proposes a fix as a follow-up commit. This requires write permissions on contents: write and careful --allowedTools:

permissions:
  contents: write
  pull-requests: write
# After the review runs and posts the comment, if no blockers:
if ! echo "$REVIEW" | grep -q "blocker"; then
  claude -p "Apply the nits from this review as minimal commits. Don't touch anything flagged warning or blocker." \
    --allowedTools "Read,Edit,Write,Bash(git:*)" \
    < /tmp/review.md
  git push origin HEAD
fi

Use with discretion. The more you let CI write back to the branch, the more you need a human reviewer on the result.

What this unlocks for the customer

By the end of a POC that used this pattern from week one:

  • Every PR comment thread is a searchable record of Claude's review + the human's response. Great for handoff.
  • Every tag has a generated release-notes doc. Great for customer's release manager.
  • Every merge to main triggers a metadata-inventory doc update. Great for customer's architect.
  • Nightly sweeps caught regressions 8 hours before the morning standup. Great for your team's sleep.

None of that is possible with Cursor alone. It needs a terminal, a shell, and an API key. That is the Claude Code advantage.

Next

On this page