Skip to content
KoishiAI
ไทย
← Back to contents
Chapter 9 / 12 · September 5, 2026

Automation — headless, schedules, links and CI

Getting Claude Code to work when nobody is typing: claude -p in a script, the three scheduling mechanisms that are genuinely different, claude-cli:// links in a runbook, GitHub Actions and the Agent SDK.

This guide is All Rights Reserved — free to read, copying/republication requires permission.

Chapter 9 | Automation Written for: anyone who wants Claude Code working with nobody sitting at the keyboard

9.1 Headless — calling it from a script with claude -p

Add -p (or --print) to run non-interactively:

claude -p "Find and fix the bug in auth.py" --allowedTools "Read,Edit,Bash"

It reads stdin, so it pipes like any other Unix command:

cat build-error.txt | claude -p 'concisely explain the root cause of this build error' > output.txt

Piped stdin is capped at 10MB. Beyond that Claude Code exits with an error and a non-zero status. For larger input, write it to a file and name the path in the prompt.

--bare is the flag anyone running CI should know about

claude --bare -p "Summarize README.md" --allowedTools "Read"

It skips auto-discovery of hooks, skills, custom commands, subagents, plugins, MCP servers, auto memory and CLAUDE.md. The benefit is the same result on every machine: a hook in a teammate’s ~/.claude, or an MCP server in the project’s .mcp.json, will not run, because bare mode never reads them.

The reverse of that is a security warning worth reading twice. Without --bare, a -p session runs the hooks in a project’s .claude/settings.json and connects the servers in its .mcp.json even in a folder you have never trusted — a -p session shows no workspace trust dialog and no per-server approval prompt.

In bare mode Claude Code never reads OAuth credentials or the keychain, so set ANTHROPIC_API_KEY yourself.

The documentation states that --bare will become the default for -p in a future release. Write today’s scripts with that in mind.

Output formats

ValueWhat you get
text (default)Plain text
jsonStructured JSON with result, session ID and metadata, including total_cost_usd
stream-jsonNewline-delimited JSON, streaming live

You can constrain the shape of the answer with --json-schema; the result lands in a structured_output field:

claude -p "Extract the main function names from auth.py" \
  --output-format json \
  --json-schema '{"type":"object","properties":{"functions":{"type":"array","items":{"type":"string"}}},"required":["functions"]}'

Permission modes under -p — this differs from what many people assume. The starting mode for -p is Manual on every plan. You have to name the mode you want:

ModeSuits
--permission-mode autoLetting the classifier review actions instead of you
--permission-mode dontAskLocked-down CI: deny anything not in an allow rule
--permission-mode acceptEditsWriting files without prompting

And when nobody is available to answer a prompt at all:

claude -p "Update the dependency pins and run the tests" --permission-mode auto --permission-prompts none

Write allow rules precisely. The space before * carries meaning:

claude -p "Look at my staged changes and create an appropriate commit" \
  --allowedTools "Bash(git diff *),Bash(git log *),Bash(git status *),Bash(git commit *)"

Bash(git diff *) allows commands beginning with git diff. Bash(git diff*), without the space, would also catch git diff-index.

Continuing a conversation

session_id=$(claude -p "Start a review" --output-format json | jq -r '.session_id')
claude -p "Continue that review" --resume "$session_id"

Exit status. The documentation states that Claude Code exits 0 on success and non-zero when the run fails, which is what scripts should branch on. The one specific case it names: SIGTERM produces exit code 143, and the turn in progress is left with no result recorded. Send SIGINT instead if you want the turn to finish.

9.2 Scheduling — three mechanisms that are not the same

This is where people pick wrong, because all three sound alike.

Routines (cloud)Desktop scheduled task/loop
Runs onThe cloudYour machineYour machine
Machine must be onNoYesYes
Session must be openNoNoYes
Survives a restartYesYesRestored on --resume if unexpired
Local file accessNo (fresh clone)YesYes
Permission promptsNone; runs autonomouslyConfigurable per taskInherited from the session
Shortest interval1 hour1 minute1 minute

/loop — the fastest way to watch something during a session

/loop 5m check if the deployment finished and tell me what happened

It takes three forms:

What you give itWhat happens
An interval and a promptRuns on a fixed schedule
A prompt onlyClaude picks the interval each round, between one minute and one hour, based on what it observed
Nothing at all (/loop)Runs the built-in maintenance prompt, or your loop.md if you have one

Units are s, m, h, d. Seconds round up to a minute, since cron’s finest granularity is one minute, and intervals that do not divide cleanly such as 7m or 90m are rounded to one that does — Claude tells you which it chose.

Press Esc while a self-paced loop is waiting to stop it.

Three traps worth knowing

  1. Seven-day expiry. A recurring task fires one last time and deletes itself seven days after creation. That ceiling exists so a forgotten loop cannot run forever. For anything longer, use Routines or a Desktop scheduled task.
  2. There is jitter. A recurring task can fire up to 30 minutes late (or up to half the interval, for tasks more frequent than hourly), so every session does not hit the API at the same moment. If timing matters, avoid :00 and :30 — use 3 9 * * * rather than 0 9 * * *.
  3. There is no catch-up. If the scheduled time passes while Claude is busy, it fires once when free, not once per missed round.

Write your own default prompt with loop.md

PathScope
.claude/loop.mdProject level; wins when both exist
~/.claude/loop.mdUser level; applies to projects with none of their own

Edits take effect on the next iteration, so you can refine it while a loop runs. Content beyond 25,000 bytes is truncated.

The tools underneath are CronCreate, CronList and CronDelete, taking standard five-field cron expressions — minute hour day-of-month month day-of-week. A session holds at most 50 tasks, and extended syntax such as L, W, ? or name aliases like MON is not supported.

All times are interpreted in your local timezone, not UTC. Disable the whole scheduler with CLAUDE_CODE_DISABLE_CRON=1.

Routines — scheduling that does not need your machine on

Type /schedule in any session to create one conversationally; /schedule list and /schedule update also exist. Routines run on Anthropic-managed infrastructure, with a minimum interval of one hour, and require a claude.ai account (see the plan table in chapter 8).

The format is claude-cli://open followed by parameters:

ParameterMeaning
qText to pre-fill in the prompt box. URL-encode it; use %0A for line breaks. Maximum 5,000 characters
cwdAbsolute path for the working directory. UNC paths, paths containing .., and invisible control characters are rejected
repoA GitHub owner/name slug. Claude Code resolves it to a local clone it has seen before; with no match it opens your home directory

If you pass both cwd and repo, cwd wins and repo is ignored, even when that path does not exist.

In an incident runbook:

## High 5xx rate on web-gateway

1. Acknowledge the page in PagerDuty.
2. [Open Claude Code in the gateway repo](claude-cli://open?repo=acme/web-gateway&q=5xx%20rate%20is%20elevated%20on%20web-gateway.)
3. Post initial findings in #incident.

From a shell:

# macOS
open "claude-cli://open?repo=acme/payments&q=review%20open%20PRs"

# Linux
xdg-open "claude-cli://open?repo=acme/payments&q=review%20open%20PRs"

On Windows PowerShell use Start-Process "claude-cli://open?...". In cmd.exe, start treats its first quoted argument as a window title, so pass an empty one first: start "" "claude-cli://open?...".

On safety: the link runs nothing by itself. It only picks a directory and fills the prompt box; nothing reaches the model until you press Enter. A Prompt from an external link warning stays below the input until you send or clear it, and for prompts over 1,000 characters it includes the character count and tells you to scroll through the whole text first, since a long prompt can push instructions off screen.

Common traps

  • Clicking does nothing — the handler is not registered yet. It registers when you send your first prompt of an interactive session, not when a session starts
  • GitHub strips the link — READMEs, issues, PRs and wikis allow only http and https, so the link renders as bare text. Put it in a code block for people to copy instead
  • It opens your home directoryrepo only resolves to clones Claude Code has already seen. Run claude in that clone once, or switch to cwd

Turn registration off entirely with disableDeepLinkRegistration set to "disable" in settings.json.

9.4 CI — GitHub Actions

The quickest setup is /install-github-app inside Claude Code, which installs the GitHub App, adds the secret and prepares the workflow pull request for you.

Or write the workflow yourself:

name: Claude Code
on:
  issue_comment:
    types: [created]
jobs:
  claude:
    if: contains(github.event.comment.body, '@claude')
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
      issues: write
      id-token: write
    steps:
      - uses: actions/checkout@v6
      - uses: anthropics/claude-code-action@v1
        with:
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}

CLI flags go through claude_args--model, --allowedTools, --max-turns and so on.

9.5 The Agent SDK

When you need more control than the CLI gives, use the packages directly. The old name “Claude Code SDK” is now “Agent SDK”.

LanguagePackage
Pythonclaude-agent-sdk
TypeScript@anthropic-ai/claude-agent-sdk
from claude_agent_sdk import query, ClaudeAgentOptions

async for message in query(
    prompt="your prompt",
    options=ClaudeAgentOptions(
        allowed_tools=["Read", "Edit", "Glob"],
        permission_mode="acceptEdits",
    ),
):
    ...

9.6 Choosing between them

If the problem isUse
A one-off call from a script or Makefileclaude -p --bare
Watching something while you work/loop
Regular work that must run with the machine offRoutines (/schedule)
Regular work that needs local filesA Desktop scheduled task
Something tied to a PR or a pushGitHub Actions
One button in an incident runbookA claude-cli:// link
An application that steers message by messageThe Agent SDK

Glossary for this chapter

TermMeaning
Headless / -pRunning non-interactively, reading stdin and writing stdout
--bareSkipping hooks, skills, MCP and CLAUDE.md so every machine gives the same result
JitterThe deliberate offset applied to scheduled fire times to spread load
RoutineA scheduled task running in the cloud, needing no machine of yours
Deep linkA claude-cli:// URL that opens a session with the prompt pre-filled