After this article you'll have a GitHub Actions workflow that triages every new issue with Claude, a nightly job that rewrites your stale changelog from real commits, and a claude
CLI step that auto-fixes failing lint on a PR branch and pushes the patch back. All three are copy-paste runnable today, and I'll show you the exact line that cost me $14 in wasted API calls before I caught it.
The blunt conclusion first: running Claude Code interactively in your terminal is great for building, but terrible for chores. Chores happen when you're asleep, in a meeting, or context-switched onto something else. I was spending roughly 70 minutes a day on three things β labeling issues, writing release notes, and fixing the same trivial lint failures β and none of it needed my brain.
The unlock is that @anthropic-ai/claude-code
ships a non-interactive mode. claude -p "prompt"
runs headlessly, prints to stdout, and exits with a status code. That's the entire bridge between "AI assistant" and "cron job". Once it runs headlessly, GitHub Actions becomes a free scheduler with secrets management, a checkout of your repo, and write access to your PRs already wired up.
My real numbers after three weeks: 6.2 hours/week reclaimed, about $9/month in Claude API spend (I'm on a metered key for CI, separate from my Max plan), and one embarrassing incident I'll get to in the pitfalls section.
The most boring chore is the highest-value to automate, because it happens on someone else's schedule. Every new issue used to sit unlabeled until I got to it. Now Claude reads the issue body, picks from my actual label set, and posts a one-paragraph triage comment.
The key design choice: I pass Claude the real label list from the repo via gh
, so it can't hallucinate priority: P0
when my labels are prio/high
. Grounding the model in real data is the difference between a useful bot and a noisy one.
name: Claude issue triage
on:
issues:
types: [opened]
permissions:
issues: write
contents: read
jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Claude Code
run: npm install -g @anthropic-ai/claude-code
- name: Triage with Claude
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BODY: ${{ github.event.issue.body }}
NUM: ${{ github.event.issue.number }}
run: |
LABELS=$(gh label list --limit 100 --json name -q '.[].name' | paste -sd ',')
SUGGESTION=$(claude -p "You are triaging a GitHub issue. \
Available labels (pick 1-3 ONLY from this list): $LABELS. \
Issue text: $BODY. \
Reply with a JSON object: {\"labels\": [...], \"comment\": \"one short paragraph\"}" \
--model claude-haiku-4-5-20251001 --output-format json | jq -r '.result')
echo "$SUGGESTION" | jq -r '.labels[]' | while read -r L; do
gh issue edit "$NUM" --add-label "$L" || true
done
echo "$SUGGESTION" | jq -r '.comment' | gh issue comment "$NUM" --body-file -
Note I use claude-haiku-4-5-20251001
here, not Opus. Triage is a classification task β Haiku does it for roughly a tenth of the cost and finishes in ~4 seconds. Reserve the expensive models for jobs that actually write code. This single decision dropped my triage cost from ~$0.03 to ~$0.004 per issue.
Everyone's changelog rots. Mine had a 5-week gap. Instead of a git hook nobody runs, I schedule a 2 a.m. job that diffs the last 24 hours of commits and asks Claude to summarize them into human-readable release notes β then opens a PR so I review before anything merges.
The interesting bit is grounding again: I don't ask Claude "what changed lately?" I hand it the commits and forbid invention. Here's the Python that builds the prompt and calls the SDK directly (cleaner than shelling out when you need to parse structured input):
import subprocess, datetime, os
from anthropic import Anthropic
def commits_since(hours=24):
since = (datetime.datetime.now(datetime.timezone.utc)
- datetime.timedelta(hours=hours)).isoformat()
out = subprocess.run(
["git", "log", f"--since={since}", "--no-merges",
"--pretty=format:%h %s"],
capture_output=True, text=True, check=True,
)
return out.stdout.strip()
def summarize(commits: str) -> str:
client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
msg = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=800,
messages=[{
"role": "user",
"content": (
"Turn these git commits into changelog bullets grouped "
"under ### Added / ### Fixed / ### Changed. "
"Use ONLY information present in the commit messages "
"β do not invent features. Commits:\n\n" + commits
),
}],
)
return msg.content[0].text
if __name__ == "__main__":
commits = commits_since(24)
if not commits:
print("No commits in window; skipping.")
raise SystemExit(0)
notes = summarize(commits)
today = datetime.date.today().isoformat()
with open("CHANGELOG.md", "r+", encoding="utf-8") as f:
old = f.read()
f.seek(0)
f.write(f"## {today}\n\n{notes}\n\n{old}")
And the workflow that runs it and opens a PR β peter-evans/create-pull-request
handles the branch and commit so you keep a human review gate:
name: Nightly changelog
on:
schedule:
- cron: "0 17 * * *" # 02:00 JST
workflow_dispatch:
jobs:
changelog:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 } # full history or --since lies to you
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install anthropic
- run: python scripts/changelog.py
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
- uses: peter-evans/create-pull-request@v6
with:
commit-message: "docs: nightly changelog"
branch: auto/changelog
title: "Nightly changelog update"
fetch-depth: 0
is not optional. The default shallow checkout grabs one commit, so git log --since
returns almost nothing and your changelog silently stays empty. I lost two nights to a "working" job that produced blank PRs because of this.
claude
auto-fix loop billed me on every push
Here's the failure I promised. My third workflow auto-fixes lint on a PR: it runs ruff
, and if it fails, hands the diff to Claude to fix and pushes the result. The naive version triggered on: push
. Claude pushes a fix β that push triggers the workflow again β ruff still flags one stylistic rule Claude and the linter disagree on β Claude "fixes" it again β push β loop. It ran 9 times in 4 minutes before I killed it, billing an Opus call each round. $14, gone, on a missing-newline argument between two robots.
Two guards fixed it permanently. First, skip the job when the actor is the bot itself. Second, only push if the working tree actually changed:
name: Claude lint autofix
on:
pull_request:
types: [opened, synchronize]
jobs:
autofix:
if: github.actor != 'github-actions[bot]' # break the loop
runs-on: ubuntu-latest
permissions: { contents: write, pull-requests: write }
steps:
- uses: actions/checkout@v4
with: { ref: ${{ github.head_ref }} }
- run: npm install -g @anthropic-ai/claude-code
- run: pip install ruff
- name: Fix lint with Claude
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
if ! ruff check . ; then
claude -p "ruff check failed. Read the ruff output above, \
edit the offending files to fix ONLY the lint errors, \
and make no behavioral changes." \
--model claude-sonnet-4-6 --permission-mode acceptEdits
fi
- name: Commit only if changed
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if ! git diff --quiet ; then
git commit -am "style: claude lint autofix"
git push
else
echo "No changes β not pushing."
fi
The --permission-mode acceptEdits
flag is what lets Claude Code edit files unattended without an interactive approval prompt; without it the CLI hangs forever in CI waiting for a keystroke. The git diff --quiet
check is your circuit breaker β if Claude decided the code was already fine, you push nothing and the cycle dies.
After three weeks the pattern that generalizes: anything where the input is text the model can be grounded in (an issue body, a commit range, a linter's stderr) and the output is reviewable (a comment, a PR, a labeled item) is a great candidate. Anything that writes to production without a human gate is not β every one of my workflows ends in a PR or a comment, never a direct merge or deploy.
Pick the model per job: Haiku for classification, Sonnet for code edits, Opus only when you genuinely need deep reasoning. Cap your CI key with a monthly budget in the Anthropic console so a runaway loop costs you $14 of annoyance instead of a $400 invoice. Start with the issue triage workflow above β it's the lowest risk, ships value on day one, and proves out your secrets setup before you let the model touch code.