Appearance
Claude GitHub Action
Set up the official Claude Code GitHub Action so @claude mentions and the claude issue label trigger Claude to respond in comments, implement changes, and open pull requests.
Design
Claude acts as a helpful human collaborator, not a naive bot. The design has two layers:
- Trigger: the workflow fires on every editor-authored event (no
@claudefilter at the workflow level). Permissive. - Judgment: Claude decides at runtime whether to respond: reply if directly addressed, participate if already invited into the thread, stay silent otherwise. Selective.
A real teammate doesn't need to be re-tagged in every reply, but they also don't respond to every comment they can see.
Trigger examples
These scenarios illustrate the trigger-then-judge model in practice:
| Actor | Event | Fires? | Responds? |
|---|---|---|---|
| Maintainer | Comments @claude on an issue | Yes | Yes |
| Maintainer | Applies the claude label to an issue | Yes | Yes |
| Maintainer | Replies in a Claude thread (no @claude) | Yes | Claude judges |
| Maintainer | Side-chats in a Claude thread (no @claude) | Yes | Usually no |
| Maintainer | Comments @claude on a closed issue | Yes | Yes |
| Stranger | Comments @claude (no write access) | No | n/a |
claude[bot] | Posts a comment | No | n/a |
Write-access gate: only users with write access (admin, maintain, write) reach the judgment layer. A gate job checks the GitHub API directly; bots and strangers skip the workflow entirely.
👀 reaction protocol: when Claude decides to respond, it reacts 👀 to every comment it intends to address (acknowledgment before action). Absence of 👀 tells the human Claude decided not to engage, without a noisy "won't respond" comment.
Image embedding: GitHub's image upload endpoint is not reachable from GitHub Actions, so pasting images directly into CI comments would produce broken links. The workflow installs an upload-image helper on PATH that uploads to R2 instead; the agent runs url=$(upload-image path/to/file.png) and embeds . Credentials are isolated to a setup step; the agent never sees them. This is core to the setup, not optional.
PR-only delivery: Claude never pushes directly to main. All code changes go through a branch and PR, with the issue number referenced in both.
Concurrency: runs are serialized per issue/PR. The second run for the same issue queues until the first finishes, then reads the first's output and decides whether to extend, replace, or skip.
Editing workflow files needs a separate token: the claude job's permissions: block scopes the GITHUB_TOKEN for the agent's normal work (contents, issues, pull requests, etc.). It deliberately does NOT try to grant access to files under .github/workflows/: GitHub blocks the GITHUB_TOKEN from creating or updating workflow files no matter what, so a workflow can't rewrite itself. There is no permissions: key that lifts this. (workflows is not a valid key in the block at all, and adding it makes the whole workflow file invalid, which startup-fails every run. The valid keys are actions, attestations, checks, contents, deployments, id-token, issues, discussions, packages, pages, pull-requests, repository-projects, and security-events.) So when the agent tries to push an edit to .github/workflows/ (re-syncing the canonical workflow, changing claude_args), the push is rejected with "refusing to allow a GitHub App to create or update workflow ... without workflows permission". To let the Action update workflow files, give it a credential that carries the workflow scope: a fine-grained or classic PAT (or a separate GitHub App installation token) stored as a secret and used in place of GITHUB_TOKEN for that push. Otherwise, make workflow-file edits from a credential that already has the scope (a maintainer locally, or sync-claude-action.sh). One more caveat for any workflow-file change: the claude-code-action OIDC token exchange refuses to run on a pull request that modifies its own workflow file (an anti-exfiltration guard that demands the PR's workflow match the default branch), so such edits only take effect once merged to the default branch.
Setup
Three components are required:
- The Claude GitHub App installed on the repo (grants access)
- An auth secret (authenticates the workflow)
- A workflow file (defines triggers, behavior, and image embedding)
Quickstart
If Claude Code is installed locally, run claude inside the repo. Then, at the Claude Code prompt, type the slash command:
/install-github-app/install-github-app is a built-in Claude Code slash command. It walks through app installation, picks an auth method (OAuth or API key), writes the secret, commits the workflow file, and creates the claude label. The steps below document the manual path.
1. Install the Claude GitHub App
Visit github.com/apps/claude and install on the target repositories.
2. Add an auth secret
Option A: Claude Pro/Max OAuth token (recommended)
Billed against your plan. One repo at a time:
sh
./skills/github/set-claude-oauth-token.sh OWNER/REPOFor multiple repos or annual rotation, use sync-claude-action.sh setup-tokens. Tokens expire after one year; when one expires, runs fail with 401. Re-run sync-claude-action.sh setup-tokens --discover to rotate. The script also writes .github/workflows/claude-token-expiry.yaml, which fires GitHub notifications at 30, 15, 7, 3, 2, 1, and 0 days before expiry via cron so no manual calendar reminder is needed.
Workflow-yaml gotcha: the expiry workflow constructs its GitHub issue body via a printf lines-as-args pattern into $RUNNER_TEMP and then gh issue create --body-file …. An inline gh issue create --body "..." with a multi-line string would parse fine as bash, but the YAML literal block (run: |) terminates on a continuation line without indent, so the whole workflow then fails YAML validation. Any future edit that adds a multi-line body should follow the same --body-file pattern documented at issue-editing.md.
Option B: Anthropic API key
Billed per token. In the repo: Settings → Secrets and variables → Actions → New repository secret, name ANTHROPIC_API_KEY.
3. Configure Actions permissions
Settings → Actions → General → Workflow permissions:
- Select Read and write permissions
- Check Allow GitHub Actions to create and approve pull requests
New repos default to read-only; without this, runs succeed but Claude can't comment or push branches.
4. Add the workflow
Install the canonical workflow:
sh
./skills/github/sync-claude-action.sh install OWNER/REPOThis writes claude-action-workflow.yaml to .github/workflows/claude.yaml and creates the claude label (color #FF6B35). The canonical workflow implements everything described in the Design section: gate job, concurrency, upload-image helper, broad tool allowlist, and a disallowlist that blocks force push, branch deletion, gh repo delete, npm publish, secret mutations, and direct-to-main push.
Image embedding secrets: the upload-image helper needs three required repo secrets. Without them it exits with "not configured" and the agent posts without images. Two optional secrets let you override the default bucket and public URL.
| Secret | Required | Description |
|---|---|---|
R2_ACCESS_KEY_ID | yes | R2 API token access key |
R2_SECRET_ACCESS_KEY | yes | R2 API token secret |
R2_ACCOUNT_ID | yes | Cloudflare account ID |
R2_BUCKET | no | Bucket name (default: img) |
R2_PUBLIC_URL | no | Public base URL (default: https://img.chriscalo.com) |
sh
export R2_ACCESS_KEY_ID=...
export R2_SECRET_ACCESS_KEY=...
export R2_ACCOUNT_ID=...
export R2_BUCKET=my-bucket # optional; omit to use default
export R2_PUBLIC_URL=https://img.example.com # optional; omit to use default
./skills/github/sync-claude-action.sh setup-r2 OWNER/REPO
# or: setup-r2 --discover (all repos that already have claude.yaml)One-time R2 setup: create an R2 bucket in Cloudflare with public access and a custom domain in front of it. Create an API token with Object Read & Write scoped to that bucket.
5. Add a CLAUDE.md (optional but recommended)
markdown
# Project conventions
- [language/framework preferences]
- Run `npm run lint` and `npm test` before opening a PR
- Match existing code style; do not reformat unrelated filesWithout it, Claude infers conventions from the codebase each run, which is slower and less consistent.
6. Test
Comment @claude please summarize this repo on a new issue and watch Actions. Claude should reply within ~30 seconds. Then apply the claude label to another issue to verify the label trigger.
To verify image embedding: comment @claude please attach a screenshot showing the structure of this repo and confirm the reply includes an inline image rather than a broken image link.
If runs fail, check the workflow logs; most failures are a missing secret, missing app installation, insufficient Actions permissions, or a missing id-token: write permission for OIDC. A run that fails to start at all (startup failure, no jobs) usually means the workflow file is invalid: a common cause is an unrecognized permissions: key (see Editing workflow files needs a separate token).
Trade-offs
- OAuth vs API key: OAuth uses your existing Claude plan (no extra billing, but GitHub usage shares the same limits as local sessions). API keys bill per token and allow per-repo cost attribution.
- Opus vs Sonnet: the workflow runs Opus by default via
--model opus, which always resolves to the latest Opus, so it never needs a version bump. Switch to Sonnet per-repo withclaude_args: "--model sonnet"(faster and cheaper, fine for simpler tasks). - CLAUDE.md vs none: without it, Claude infers conventions each run (costs tokens, less consistent). A short CLAUDE.md pays for itself quickly.
See also
- Claude Code Remote Workflows: drive a local Claude Code session from mobile instead of running it as a GitHub Action.
- Claude Code Settings: local configuration and the CLAUDE.md conventions that the Action reads the same way.
- Coding Agents: full skill index for AI coding agent setup.