# Automating Daily Dev Chores with Claude Code and GitHub Actions: 3 Workflows I Use

> Source: <https://dev.to/_7fb6011b57d383122b5a/automating-daily-dev-chores-with-claude-code-and-github-actions-3-workflows-that-saved-me-6-hours-54gh>
> Published: 2026-06-06 04:19:20+00:00

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.

```
# .github/workflows/triage.yml
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):

``` python
# scripts/changelog.py
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:

```
# .github/workflows/changelog.yml
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:

```
# .github/workflows/autofix.yml
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.
