Claude Code Task

claude-code-task@1.0.0 Template

Give Claude Code a task in your repo and get the result back as a draft pull request.

Start from it

flightstack template use claude-code-task

What it does

Runs Claude Code headless in the checked-out repository with the task you give it, an explicit tool allowlist and a dollar cap. If it changes files, the job commits them to a new branch, pushes, and opens a (draft) pull request for a human to review. It never pushes to the base branch.

Inputs

task string required
What Claude should do, written as you would brief a teammate.
repository string required
GitHub repository as owner/name, usually "${{ context.repository }}".
baseBranch string default main
Branch to start from and open the pull request against.
branchName string required
New branch for the change, e.g. "claude/fix-${{ context.runNumber }}". Must not be the base branch.
allowedTools string default Read Edit Write Glob Grep
Space-separated Claude Code tools Claude may use without asking. Anything else is denied. Add narrow Bash patterns for the commands the task needs, e.g. "Bash(npm test *)". Avoid a bare "Bash".
model string default claude-opus-5-5
Claude model id.
maxBudgetUsd number default 5
Hard cap on API spend for the run, in US dollars.
openPullRequest boolean default true
Open a pull request. Off pushes the branch only.
draft boolean default true
Open the pull request as a draft.
claudeCodeVersion string default latest
Version of @anthropic-ai/claude-code to run.

Definition

name: Claude Code Task
description: >-
  Give Claude Code a task in the repository - fix failing tests, bump a
  dependency, update docs - and get the result back as a pull request.

triggers:
  - type: manual
  - type: schedule
    cron: "0 6 * * 1"

jobs:
  deps:
    name: Weekly dependency bump
    type: plugin/claude-code-task
    with:
      repository: "${{ context.repository }}"
      baseBranch: main
      branchName: "claude/deps-${{ context.runNumber }}"
      task: >-
        Upgrade the Dart dependencies in pubspec.yaml to their latest
        compatible versions, run `flutter pub get` and `flutter test`, and fix
        anything the upgrade breaks. Keep the change minimal.
      allowedTools: "Read Edit Write Glob Grep Bash(flutter pub *) Bash(flutter test *) Bash(dart format *)"
      maxBudgetUsd: 5

plugin:
  name: claude-code-task
  displayName: Claude Code Task
  description: >-
    Runs Claude Code headless in the checked-out repository with the task you
    give it and an explicit tool allowlist. If it changes files, the job
    commits them to a new branch, pushes, and opens a pull request for a
    human to review. It never pushes to the base branch.
  category: utility

  requiredTools:
    - name: nodejs
      version: ">=20.0.0"

  supportedPlatforms:
    - linux
    - macos

  parameters:
    type: object
    properties:
      task:
        type: string
        description: What Claude should do, written as you would brief a teammate.
      repository:
        type: string
        description: GitHub repository as owner/name, usually "${{ context.repository }}".
      baseBranch:
        type: string
        default: main
        description: Branch to start from and open the pull request against.
      branchName:
        type: string
        description: >-
          New branch for the change, e.g. "claude/fix-${{ context.runNumber }}".
          Must not be the base branch.
      allowedTools:
        type: string
        default: "Read Edit Write Glob Grep"
        description: >-
          Space-separated Claude Code tools Claude may use without asking.
          Anything else is denied. Add narrow Bash patterns for the commands
          the task needs, e.g. "Bash(npm test *)". Avoid a bare "Bash".
      model:
        type: string
        default: claude-opus-5-5
        description: Claude model id.
      maxBudgetUsd:
        type: number
        default: 5
        description: Hard cap on API spend for the run, in US dollars.
      openPullRequest:
        type: boolean
        default: true
        description: Open a pull request. Off pushes the branch only.
      draft:
        type: boolean
        default: true
        description: Open the pull request as a draft.
      claudeCodeVersion:
        type: string
        default: latest
        description: Version of @anthropic-ai/claude-code to run.
    required: [task, repository, branchName]

  script: |
    set -euo pipefail

    for tool in git curl jq npx; do
      command -v "$tool" >/dev/null 2>&1 || { echo "$tool is required on this agent." >&2; exit 1; }
    done
    if [ -z "${ANTHROPIC_API_KEY:-}" ]; then
      echo "Set ANTHROPIC_API_KEY as a secret variable for this pipeline." >&2
      exit 1
    fi
    if [ -z "${GITHUB_TOKEN:-}" ]; then
      echo "Set GITHUB_TOKEN (contents and pull-requests write) as a secret variable." >&2
      exit 1
    fi

    repo="{{repository}}"
    base="{{baseBranch}}"
    branch="{{branchName}}"
    if [ "$branch" = "$base" ]; then
      echo "branchName must differ from baseBranch - this step never pushes to $base." >&2
      exit 1
    fi

    task="$(cat <<'FS_TASK'
    {{task}}
    FS_TASK
    )"

    git fetch --quiet origin "$base"
    git checkout --quiet -B "$branch" "origin/$base"
    git config user.name "Claude (FlightStack)"
    git config user.email "claude-bot@users.noreply.github.com"

    guard="You are running unattended in CI. Nobody can answer questions, so make reasonable decisions and finish the task. Do not commit, push, or change git configuration - the pipeline commits your file changes afterwards. When you are done, reply with a short summary of what you changed and anything a reviewer should check."

    result="$(mktemp)"
    echo "--- claude -p (model {{model}}, budget {{maxBudgetUsd}} USD)"
    set +e
    npx --yes "@anthropic-ai/claude-code@{{claudeCodeVersion}}" \
      -p "$task" \
      --model "{{model}}" \
      --permission-mode dontAsk \
      --allowedTools "{{allowedTools}}" \
      --append-system-prompt "$guard" \
      --max-budget-usd "{{maxBudgetUsd}}" \
      --output-format json > "$result"
    claude_status=$?
    set -e

    summary="$(jq -r '.result // empty' "$result" 2>/dev/null || true)"
    cost="$(jq -r '.total_cost_usd // empty' "$result" 2>/dev/null || true)"
    echo "$summary"
    [ -n "$cost" ] && echo "cost: \$$cost"
    if [ "$claude_status" -ne 0 ] || [ "$(jq -r '.is_error // false' "$result" 2>/dev/null)" = "true" ]; then
      echo "Claude Code did not finish the task (exit $claude_status)." >&2
      exit 1
    fi

    if [ -z "$(git status --porcelain)" ]; then
      echo "Claude made no file changes - nothing to commit."
      fs-output changed false
      exit 0
    fi

    git add -A
    git commit --quiet -m "$(printf '%s\n\n%s' "Claude Code: $(echo "$task" | head -n 1 | cut -c1-60)" "$summary")"
    git push --quiet --force-with-lease \
      "https://x-access-token:$GITHUB_TOKEN@github.com/$repo.git" "HEAD:refs/heads/$branch"
    echo "pushed $branch"
    fs-output changed true
    fs-output branch "$branch"

    if [ "{{openPullRequest}}" != "true" ]; then
      exit 0
    fi

    body="$(printf '%s\n\n---\n**Task**\n\n%s\n\n<sub>Opened by Claude Code ({{model}}) via FlightStack. Review before merging.</sub>' "$summary" "$task")"
    payload="$(jq -n \
      --arg title "Claude Code: $(echo "$task" | head -n 1 | cut -c1-60)" \
      --arg head "$branch" --arg base "$base" --arg body "$body" \
      --argjson draft {{draft}} \
      '{title: $title, head: $head, base: $base, body: $body, draft: $draft}')"
    pr_url="$(curl -sS --fail-with-body -X POST \
      -H "Authorization: Bearer $GITHUB_TOKEN" \
      -H "Accept: application/vnd.github+json" \
      -H "X-GitHub-Api-Version: 2022-11-28" \
      "https://api.github.com/repos/$repo/pulls" -d "$payload" | jq -r '.html_url')"
    echo "opened $pr_url"
    fs-output pullRequestUrl "$pr_url"