Skip to content

Day 22 of 52 · about 20 min · Automation

Run Claude Code from a script

Once a task works in a session, a script can run it the same way with nobody at the keyboard. That is how Claude gets into build scripts and CI, and it starts with one flag: -p.

You’ll finish with

  • One-off answers with claude -p, including piped input
  • Runs that may use only the tools you list
  • A Node script that reads Claude’s answer as JSON and exits 0 or 1

You’ll need

  • Claude Code installed and logged in (Day 1)
  • The practice project from the Day 22 kit, or your own project. Every command here works the same in Bash, Zsh and PowerShell.

Before you start

Tell us about your setup and the steps change to fit it. Your answers stay in this browser, and you can change them at any time.

Which system are you on?

Commands differ between systems. Pick the one you’ll run Claude Code on.

The steps

One step at a time. When a step’s check matches, tick it and the next one opens.

0 of 4 steps done0%
  1. Step 1 of 4: Ask one question

    In the project folder, pass a question after `claude -p`.
    Terminal

    Run: claude -p "Which command runs this project's tests?"

    What happens: Claude reads the project, prints its answer and exits.

    One prompt in, one answer out, and no session to close.

    Check it worked

    The answer names the test command (npm test in the practice project). echo $? (PowerShell: $LASTEXITCODE) prints 0.

  2. Step 2 of 4: Pipe another command’s output in

    Send a command’s output to Claude on stdin and ask about it.

    claude -p reads stdin, so any command’s output can go to Claude. Here it is a quote; in your own project, pipe a log or git diff.

    Terminal

    Run: node src/cli.js design:10 dev:24 --discount 10 | claude -p "Check the maths in this quote. Reply OK or name the wrong line."

    The quote travels on stdin; the instruction travels after -p.

    Check it worked

    Claude replies OK. The sample quote is correct.

  3. Step 3 of 4: Pre-approve one tool and get JSON back

    List the tools Claude may use without asking, and ask for JSON output.

    With -p, nobody is there to answer a permission prompt, so anything that would prompt is refused. --allowedTools approves exactly what the task needs.

    Terminal

    Run: claude -p "Run the tests and say how many passed" --allowedTools "Bash(npm test *)" --output-format json

    What happens: One JSON object: the answer in result, plus session_id and an estimated cost in total_cost_usd.

    Bash(npm test *) approves npm test, with or without arguments, and nothing more.

    Check it worked

    The JSON’s result field says how many tests passed.

  4. Step 4 of 4: Let a script read the answer

    Run the kit script, which asks for JSON in a fixed shape.

    The kit’s script pipes the quote to claude -p with --json-schema, then reads ok and problems from the structured_output field.

    scripts/check-quote.mjsJavaScript
    /**
     * Day 22: run Claude Code from a script.
     *
     *   node scripts/check-quote.mjs
     *
     * Prints the sample quote with src/cli.js, pipes it to `claude -p` and asks
     * Claude to check the arithmetic. `--output-format json` with `--json-schema`
     * makes Claude Code return the answer in the `structured_output` field, so
     * this script reads two fields instead of parsing prose.
     *
     * Exit codes: 0 the quote checks out, 1 Claude found a problem, 2 Claude
     * could not be run or gave no answer in the expected shape.
     *
     * Flags and fields: https://code.claude.com/docs/en/headless (checked
     * 9 October 2026).
     *
     * On Windows, Node starts `claude.exe` from the native installer. An npm
     * install provides `claude.cmd`, which Node can't start without a shell.
     */
    import { spawnSync } from 'node:child_process';
    import { fileURLToPath } from 'node:url';
    
    const ROOT = fileURLToPath(new URL('..', import.meta.url));
    const SAMPLE = ['src/cli.js', 'design:10', 'dev:24', '--discount', '10'];
    
    const SCHEMA = JSON.stringify({
      type: 'object',
      properties: {
        ok: { type: 'boolean' },
        problems: { type: 'array', items: { type: 'string' } },
      },
      required: ['ok', 'problems'],
    });
    
    const PROMPT = [
      'The text on stdin is a price quote.',
      'Each line is hours times an hourly rate.',
      'The discount is a percentage of the subtotal.',
      'VAT is a percentage of the amount after the discount.',
      'Check every line and every total.',
      'Set ok to true only if all of them are right.',
      'List each wrong line in problems, with the value it should have.',
    ].join(' ');
    
    function fail(message) {
      console.error(message);
      process.exit(2);
    }
    
    const quote = spawnSync(process.execPath, SAMPLE, { cwd: ROOT, encoding: 'utf8' });
    if (quote.status !== 0) fail(`Could not print the sample quote:\n${quote.stderr}`);
    
    const run = spawnSync('claude', ['-p', PROMPT, '--output-format', 'json', '--json-schema', SCHEMA], {
      cwd: ROOT,
      input: quote.stdout,
      encoding: 'utf8',
      maxBuffer: 10 * 1024 * 1024,
    });
    
    if (run.error) fail(`Could not start claude (${run.error.code}). Check that \`claude --version\` works in this terminal.`);
    if (run.status !== 0) fail(`claude -p exited with code ${run.status}:\n${(run.stdout + run.stderr).trim()}`);
    
    let answer;
    try {
      answer = JSON.parse(run.stdout).structured_output;
    } catch {
      fail(`claude -p did not print JSON:\n${run.stdout.trim()}`);
    }
    if (!answer || typeof answer.ok !== 'boolean' || !Array.isArray(answer.problems)) {
      fail(`No answer in the expected shape:\n${run.stdout.trim()}`);
    }
    
    if (answer.ok) {
      console.log('Quote checked: no problems found.');
    } else {
      console.log('Quote checked: Claude found problems.');
      for (const problem of answer.problems) console.log(`- ${problem}`);
      process.exitCode = 1;
    }
    
    Terminal

    Run: node scripts/check-quote.mjs

    Output: Quote checked: no problems found.
    Exit code 0 means the quote checks out, 1 means Claude found a problem, 2 means Claude couldn’t run.

    Check it worked

    The script prints Quote checked: no problems found. and exits with 0.

    If it didn’t work

    The script says it could not start claude

    Check that claude --version works in the same terminal. On Windows, Node can start the native installer’s claude.exe but not npm’s claude.cmd: use the Day 1 installer.

    The result says you are not logged in

    /login doesn’t work with -p. Start claude once, run /login, then try again.

Finish · 4 of 4 steps to go

Claude runs without you at the keyboard

In CI, add --bare so local hooks, skills and CLAUDE.md can’t change the run.

What you’ll have checked

  • claude -p answers and exits with code 0
  • Piped output reaches Claude
  • node scripts/check-quote.mjs prints its result and exits 0

Lesson updates by email

Get lesson updates by email

Leave your email and we’ll tell you when we add or update a lesson.

We’ll email you when we add or update a lesson. We use your email only for this. Privacy policy

Claude Code for your team

Want Claude Code set up across your team?

HorizonLux sets up and repairs Claude Code for teams: CLAUDE.md rules, hooks that enforce them and reviewed MCP servers. Book a free call to talk it through.

Questions

Questions about Day 22

Can --bare use my Claude subscription?

No. Bare mode never reads your subscription login, so set `ANTHROPIC_API_KEY` with a key from the Claude Console first.

Sources

Checked against the official Claude Code docs on .

Independent resource, not affiliated with Anthropic. HorizonLux wrote this lesson; Claude Code is made by Anthropic.