Everything you do in a Claude Code session, you can do from a script. claude -p takes a prompt, runs it without the interface, prints the answer and exits with a code your script can test. That turns Claude into a command: pipe a build log in, get the root cause out; ask for JSON and read a field. On Windows the only extra work is PowerShell’s quoting.
This post covers the basic call, piping input, structured output into ConvertFrom-Json, letting tools run without a prompt, --bare for runs that must behave the same on every machine, continuing a conversation across calls, and the quoting rules. Everything is from the current docs, linked at the end. Claude Code needs to be installed and logged in first: Install Claude Code on Windows, Native and in WSL.
The Basic Call
cd C:\path\to\your\project
claude -p "What does the Build-Release.ps1 script do?"
# exit code: 0 on success, non-zero when the run failed
claude -p "Summarise README.md"
if ($LASTEXITCODE -ne 0) { Write-Warning "claude failed with $LASTEXITCODE" }
-p is short for --print. The run reads the same CLAUDE.md, settings, hooks and MCP servers an interactive session would, from the folder you run it in. 2 things it does not do: it skips the workspace trust dialog, so only run it in folders you trust, and a settings file that fails validation is silently ignored rather than reported.
Pipe Something In
Non-interactive mode reads stdin, so the input can be whatever a PowerShell pipeline produces. The prompt then describes what to do with it:
# a log file Get-Content .\build-error.txt -Raw | claude -p "Explain the root cause of this build error in 3 lines" # the output of a command git diff main | claude -p "List any typos in this diff as file:line and the fix. Nothing else." # save the answer Get-Content .\deploy.log -Raw | claude -p "Summarise what failed" | Set-Content .\summary.txt
Piped input is capped at 10 MB; for anything bigger, write it to a file and name the file in the prompt. Piping the diff also means Claude does not need Bash permission to read it, which keeps the run prompt-free without allowing anything.
Get JSON Out
--output-format json returns one object with the answer in result, plus the session ID, the cost estimate and usage. In PowerShell, collect the output and parse it:
$raw = claude -p "Summarise this project in 2 sentences" --output-format json $r = $raw | Out-String | ConvertFrom-Json $r.result $r.session_id $r.total_cost_usd
For output in a shape you define, add --json-schema. The answer lands in structured_output, and the docs say to check the field is present before using it, because a run that declines returns none. Put the schema in a variable with single quotes so PowerShell leaves the braces alone:
$schema = '{"type":"object","properties":{"functions":{"type":"array","items":{"type":"string"}}},"required":["functions"]}'
$r = claude -p "List the function names defined in Build-Release.ps1" --output-format json --json-schema $schema |
Out-String | ConvertFrom-Json
if ($r.structured_output) { $r.structured_output.functions }
--output-format stream-json emits one JSON object per line as the run progresses, for a script that wants to show progress; the last line is the same result object.
Let Tools Run Without a Prompt
From version 2.1.302 a -p run starts in Manual mode, and with nobody there to answer, anything that needs approval is denied. Say what is allowed up front:
# name the tools, with the same rule syntax as settings.json claude -p "Run the Pester tests and fix any failures" --allowedTools "Bash,Read,Edit" # or narrower claude -p "Look at my staged changes and write a commit" --allowedTools "Bash(git diff *),Bash(git log *),Bash(git status *),Bash(git commit *)" # or set a baseline mode for the run claude -p "Apply the lint fixes" --permission-mode acceptEdits claude -p "Update the dependency pins and run the tests" --permission-mode auto --permission-prompts none
--allowedTools uses the rule syntax from the permissions post, trailing space and * included. --permission-prompts none tells Claude nobody can approve anything, so it does not retry a denied call; it is for scheduled jobs. --permission-mode dontAsk is the locked-down version: anything that would prompt is denied and the run carries on.
Make the Run Repeatable With –bare
--bare skips the hooks, skills, plugins, MCP servers, auto memory and CLAUDE.md that a normal session discovers, so a script behaves the same on every machine and starts faster. The docs recommend it for scripts and CI and say it will become the default for -p. It needs an API key rather than your subscription login:
$env:ANTHROPIC_API_KEY = "<your-key>" claude --bare -p "Summarise README.md" --allowedTools "Read"
Anything you still want has to be passed explicitly: --append-system-prompt for instructions, --settings for a settings file, --mcp-config for servers. Background tasks and subagents are off in bare mode, and a command that hits its timeout stops instead of continuing in the background.
Continue a Conversation Across Calls
claude -p "Review this codebase for performance issues" claude -p "Now focus on the database queries" --continue claude -p "Write a summary of everything found" --continue # or capture the ID and resume that exact session, from any folder $id = (claude -p "Start a review" --output-format json | Out-String | ConvertFrom-Json).session_id claude -p "Continue that review" --resume $id
A -p session does not show in the interactive picker, so the ID is how you get back to it, and claude --resume <id> opens it interactively if you want to take over. The sessions post has the rest. --no-session-persistence skips writing the transcript for a run you will never resume.
Quoting in PowerShell
The prompt is one argument, so it needs quotes, and PowerShell expands $ inside double quotes. Single quotes for anything with a dollar sign or a JSON brace, double quotes when you want a variable in the prompt, and a here-string for anything long:
# $PSVersionTable would be expanded inside double quotes; single quotes keep it literal claude -p 'Explain what $PSVersionTable.PSVersion returns' # a variable you do want expanded $file = "Build-Release.ps1" claude -p "Explain what $file does" # a long prompt $prompt = @' You are a typo linter. For each typo in the diff, report file:line on one line and the issue on the next. Return nothing else. '@ git diff main | claude -p $prompt
A prompt that starts with / runs a skill, so claude -p "/review-staged" works and the skills post is where that comes from. Commands that only exist in the interface, /login for one, do not.
Schedule It
A -p call is a line in a .ps1, and a .ps1 runs from Task Scheduler. A nightly summary of what changed in a repo:
# nightly-summary.ps1
Set-Location C:\path\to\your\project
$since = (Get-Date).AddDays(-1).ToString('yyyy-MM-dd')
$log = git log --since=$since --stat
if (-not $log) { return }
$summary = $log | claude -p 'Summarise these commits for a status email. Plain text, 5 lines at most.' --output-format json --permission-prompts none |
Out-String | ConvertFrom-Json
$summary.result | Set-Content "D:\reports\git-summary-$since.txt"
How to Run PowerShell Scripts with Task Scheduler covers the scheduled task side, and the run needs to happen under the Windows account that is logged in to Claude Code, or with ANTHROPIC_API_KEY set and --bare.
Other Considerations
Cost shows in the JSON. total_cost_usd is a client-side estimate at list price and can differ from the bill, and on a subscription it is informational. With --continue it reports the whole conversation’s total, not just this call.
A budget cap. --max-budget-usd stops the run when the estimate passes the amount, and --max-turns caps the agentic loop; both are for API-key runs you leave unattended.
Stopping it. Ctrl+C ends the turn. A run killed from Task Manager or Stop-Process leaves the turn unfinished and exits 143.
Model and effort work as flags too: --model sonnet for a cheap summarise job, --effort low likewise.
Frequently Asked Questions
Does claude -p use my subscription or an API key?
Your login, unless you pass --bare, which never reads it and needs ANTHROPIC_API_KEY. Either way the run counts against the account that pays.
Why was a command denied in my script?
A -p run starts in Manual mode and nobody is there to approve. Name the tools in --allowedTools, or set --permission-mode acceptEdits or auto.
Can I read the answer as an object in PowerShell?
Yes: --output-format json, then Out-String | ConvertFrom-Json. The text is in .result; with --json-schema your shape is in .structured_output.
Why does my prompt come out with the variable name missing?
Double quotes expanded a $. Use single quotes for a literal prompt, or a single-quoted here-string @' ... '@ for a long one.
Does the run pick up my CLAUDE.md and hooks?
Yes, unless you pass --bare. That includes hooks and MCP servers in a project’s committed files, even in a folder you have never trusted, which is why the docs say not to run -p in a folder you have not read.
Related
- Claude Code on Windows: The Complete Guide
- Install Claude Code on Windows, Native and in WSL
- Claude Code Permissions: Allow and Deny Rules in settings.json
- Resume a Claude Code Session With –continue and –resume
- Custom Slash Commands in Claude Code With Skills
- How to Run PowerShell Scripts with Task Scheduler
- Claude Code Hooks: Run a Check After Every Edit
- Claude Code docs: run Claude Code programmatically (claude -p)
- Claude Code docs: CLI reference
- Microsoft Learn: ConvertFrom-Json
If SQL Server is part of your day job, my current work lives over at sqldba.blog: production DBA scripts, an error library and the SSMS guide.
Leave a Reply