Chapter 17: Headless Mode — Claude as a One-Shot Command
Until this chapter, Claude has been a conversation. You open a session, you talk, Claude works, you approve, you continue. That is one mode. There is another mode where Claude is not a conversation at all — it is a single command you run from a script. You give it one instruction, it does the work in one pass, and it exits.
This mode has an unfortunate name: headless mode. The “head” the name refers to is the interactive interface — the chat window with prompts, approvals, and color. Headless means “no interface, no prompts, no human watching.” It is Claude as a Unix tool, the same way grep or curl are Unix tools. Read input, do the work, write output, exit.
This is the mode that lets you put Claude inside a cron job, a GitHub Action, or a nightly pipeline. It is also the mode that demands the most discipline.
The -p Flag
The whole feature is one flag: -p (short for print). The documentation calls it “headless mode,” “non-interactive mode,” or “print mode” depending on the page. All the same thing.
claude -p "fix the failing tests in this repo"
When you run that, Claude does not open the interactive chat. It reads your instruction, runs through its agentic loop (read files, edit code, run tests) until it decides it is done, prints the final answer to the terminal, and exits with a status code — zero if it succeeded, non-zero if it failed.
No prompts. No “are you sure?” pop-ups. No human in the loop. That last part is the whole point of the feature and also the whole danger of it.
🧠 Remember.
-pturns Claude from a conversational tool into a command-line tool. It is what you reach for when no human is watching. Anything you would not trust to run without supervision should not go through-p.
Allowlists Are Mandatory
Because there is no human to approve any action, you have to pre-approve them. The flag for this is --allowedTools:
claude -p "summarize today's pipeline errors" \
--allowedTools "Read,Grep,Bash(grep:*)"
This says: Claude may read files, grep them, and run the grep command from bash. Nothing else. Try to write a file? Denied. Try to rm -rf? Denied. Try to call out to the internet? Denied.
There is an opposite flag, --disallowedTools, which blocks the tools you name while leaving everything else to the normal rules. Do not build your automation on it. Allowlist surgically; never blanket-allow. A blanket allow is how people lose data overnight to a prompt that took a wrong turn.
⚠️ Warning. Combining
-pwith--dangerously-skip-permissions(the nuclear flag from Chapter 4) is how clinicians lose work, money, or both. There is no human in the loop to catch the wrong turn. If you find yourself reaching for it, the right answer is to fix your allowlist, not to disable safety.
Piping In, Piping Out
To explain the next bit, two pieces of Unix vocabulary.
A pipe is the | character on your keyboard. It connects two commands so the output of the first becomes the input of the second. cat error.log | grep "WARN" means “print the file, then pass the printed text to grep, which filters for lines containing WARN.” The pipe is the joinery.
stdin (“standard input”) is whatever stream of text a command reads from. When you pipe one command into another, the second command’s stdin is whatever the first command produced. stdout (“standard output”) is the stream the command writes its results to — by default, your terminal screen.
Because -p reads stdin and writes stdout, it composes with every Unix tool you already have:
# Pipe a log file in, get an explanation out
cat error.log | claude -p "explain this stack trace and suggest a fix"
# Pipe a git diff in, get a code review out
git diff main | claude -p "review this change for SQL injection risks"
# Pipe Claude's output into a file
claude -p "list every TODO in this repo" --allowedTools "Read,Grep" | tee todos.txt
This is the Unix philosophy — small programs that read text and write text, glued together with pipes — applied to an AI agent.
💡 Tip. Add
--output-format jsonto get structured output you can parse in a script. The JSON includes Claude’s final message, token counts, and the run’s cost. Perfect for logging or for feeding into a second pipeline stage. (There is also--output-format stream-json, which emits every intermediate step as it happens.)
CI/CD, Defined
CI/CD stands for continuous integration / continuous delivery. It is the umbrella term for automated checks that run every time code changes — tests run, security scans, code formatters, build verification. The “continuous” part means it happens on every change, not on a release cadence.
The most common CI/CD platform is GitHub Actions — a system that runs scripts on GitHub’s servers whenever something happens in a repo (a new commit, a new pull request, a scheduled time). The shape is: a YAML file in your repo says “when someone opens a pull request, run these commands.” GitHub spins up a fresh virtual machine, runs the commands, reports pass or fail.
The poster-child use case for headless Claude is a GitHub Action that reviews every pull request:
claude -p "Review the diff in this PR for bugs, security issues, and KHCC \
clinical-pipeline conventions. Post your findings as inline review comments \
using the gh CLI." \
--allowedTools "Read,Grep,Bash(gh:*)"
Claude reads the diff, runs gh pr view and gh pr diff to gather context, evaluates the change, and uses gh pr review --comment to leave inline findings. No human at a console. The Action runs in seconds-to-minutes per PR.
The same shape works for nightly migration sanity checks, dependency-update triage, stale-issue labelling — any place where the work is small, the criteria are clear, and the cost of one wrong answer is recoverable.
🔧 Technical Stuff. Inside a GitHub Action, Claude Code looks for the
ANTHROPIC_API_KEYenvironment variable. Store it as a GitHub repo secret (Settings → Secrets and variables → Actions). Never echo it into logs. The installation step is one line:curl -fsSL https://claude.ai/install.sh | bash. That is the whole setup.
Cron Jobs, Defined
A cron job is a scheduled command. The name comes from cron, the program on Linux and Mac that reads a schedule file and runs commands at the times you specify. On Windows the equivalent is Task Scheduler; on macOS the modern equivalent is launchd. Same idea on all three: at a fixed time (every morning at 6, every Sunday at midnight, every five minutes), run a script.
The medical analogy: cron is the scheduler that runs the EMR’s nightly batch jobs — the overnight charge-poster, the daily census refresh, the weekly utilization report. None of those run because a human pressed a button. They run because someone wrote a schedule.
To schedule Claude in headless mode, you write a small shell script that calls claude -p, then schedule the shell script in cron:
# /etc/cron.d/aidi-nightly
0 6 * * * aidi-bot /opt/aidi/scripts/nightly-summary.sh >> /var/log/aidi-claude.log 2>&1
The five fields at the start (0 6 * * *) mean “at minute 0 of hour 6, every day of the month, every month, every day of the week.” So this runs at 6 AM daily. The script’s standard output and errors are appended to a log file. The exit code captures pass or fail.
🧠 Remember. Cron plus
-pis how Claude Code goes from “an interactive tool I open when I sit down” to “a service that runs whether I am at my desk or not.” This is the leap from coding helper to teammate.
The Safety Math
Here is the rule worth memorizing:
Headless + surgical allowlist = safe automation. Headless + skip-permissions = uncontrolled execution.
Every -p invocation should look like a recipe: a tight prompt, a tight allowlist, a tight output destination. The moment you find yourself writing claude -p "..." --dangerously-skip-permissions in a cron job, step back and ask which specific tools the job actually needs. The answer will almost always be a much shorter list than “all of them.”
⚠️ Warning. A headless Claude that can write files and run arbitrary bash, scheduled at 3 AM, is functionally indistinguishable from a malicious script someone wrote on purpose. Treat the allowlist like a hospital firewall rule. Default deny. Allow only what is required.
The KHCC Example
Here is the nightly job running on the AI Office bastion server at 6:00 AM every weekday.
Recall from Chapter 0.5 that VistA is KHCC’s electronic medical record — the source of truth for patient data, refreshed nightly into Databricks tables. Each night at about 5:30 AM, a fresh VistA extract lands in a folder called /data/vista/incoming/. The AI Office team wants a one-page summary in their inbox by the time they sit down at their desks: row counts per table, new patient counts, columns whose null rate changed sharply, and a flag if any column went unexpectedly to 100% null (which usually means an upstream column was renamed and a pipeline is about to break).
The script:
#!/bin/bash
# /opt/aidi/scripts/vista-morning-summary.sh
set -euo pipefail
cd /data/vista/incoming
claude -p "Compare today's VistA extract files to yesterday's. \
For each file: report row count delta, new column count, columns whose null \
rate changed by more than 5 percentage points. Flag anything that looks like \
a schema rename. Output as markdown. Then send the markdown body to the AIDI \
team list via the 'aidi-email' CLI tool." \
--allowedTools "Read,Grep,Bash(ls:*),Bash(wc:*),Bash(head:*),Bash(aidi-email:*)" \
--output-format text \
> /var/log/aidi/vista-summary-$(date +%F).md
Notice the tight allowlist: Claude may read files, grep them, run a few read-only bash commands (ls, wc, head), and send one email through the internal AI Office mailer. It cannot write to the data folder. It cannot delete anything. It cannot connect anywhere except the mailer. It cannot run arbitrary bash.
By 6:05 AM the AI Office team has the markdown summary in their inbox. The on-call data engineer sees it on her phone before she has poured her first coffee. If the summary is empty or the script failed, the cron log captures it. If there is a real schema drift, it surfaces before any downstream pipeline (the AKI alerter, the pathology extractor) runs against the new extract.
💡 Tip. For any new headless job, run it interactively first (without
-p), and watch which tools Claude actually reaches for. That observed set of tools is your allowlist. Do not guess at it.
Try This
- In a sandbox folder with a couple of files, run:
echo "list every TODO comment" | claude -p --allowedTools "Read,Grep"Confirm you get a clean text answer with no interactive prompts at all.
- Add
--output-format jsonand pipe the result throughjq .to see the structured response. Find thetotal_cost_usdfield — that is the number you will use to budget your cron jobs. - Write a one-line cron job (or Windows scheduled task) that runs
claude -p "summarize what changed in this repo in the last 24 hours"and writes the output to a file. Read the file the next morning. - Now imagine that same job had run with
--dangerously-skip-permissionsand a buggy prompt. That thought should make you uneasy. Good — that unease is the right calibration.
Watch Out
- No human means no recovery. A headless run that veers off course will keep going off course until it exits, hits an allowlist wall, or burns its turn limit. Set
--max-turns(for example,--max-turns 20) on any job that should be short. - Costs add up. A nightly job running Opus across a 200-file repo can be $5–$20 per run. That is $150–$600 per month for a single job. Use Sonnet for routine jobs; reserve Opus for jobs where the quality genuinely matters. Check the
total_cost_usdfield in the JSON output. - Secrets in prompts. Anything you put in the
-pprompt, or in files Claude reads, is sent to Anthropic. Do not embed PHI, real MRNs, or API keys. For any KHCC pipeline, encode MRNs with Optimus (Chapter 0.5) before anything reaches the prompt. - Surgical allowlists, every time. If you find yourself blanket-allowing
Bash, you are one prompt injection away from a bad day.