Files
shiro-neko/docs/headless.md
T
Muhammad Zakir Ramadhan 2fa6ee247b Add batch reads, @file completion, interruptible commands, tool sets
Tools, six built-in to fourteen:
- read_many_files: up to 20 paths read concurrently, each with its own window.
  An unreadable path is reported in its own block instead of throwing.
- multi_edit: several edits to one file, validated in memory first so a late
  failure cannot leave the file half-written.
- list_dir: ignore-aware depth-limited tree.
- git_status/diff/log/show/blame: read-only, spawned with a fixed argv rather
  than a shell string, which is what makes them safe to auto-approve.

toolSets gates them. core is always on; edit-plus and git are optional. A
disabled set reaches neither the wire nor the system prompt, since a prompt
naming an absent tool teaches calls that cannot succeed.

Interface:
- Reasoning streams to a collapsed panel, ctrl-r expands, dropped when the turn
  ends: it is progress, not the answer.
- The tool in flight is named from tool-input-start, before its arguments finish
  streaming, and cleared on its result.
- Prompts typed mid-turn queue and drain in order. esc clears the queue as well
  as aborting.
- @ opens a path picker fed by the ignore-aware walker. Prefix matches rank
  above substring matches, so @src/ means "under src/". The walk runs on the
  first @, not at startup.

ctrl-c kills the command in flight and keeps the turn. The call throws rather
than returning, so the model cannot read a killed command as one that ran and
failed on its own terms. The kill takes the whole process tree: killing cmd /c
alone left the real command holding both pipes open, so the read never returned
and the interrupt did nothing for 19 seconds.

Two pruning fixes:
- A tool result whose tool call was pruned is now dropped with it. Pruning
  counts messages, so the cut landed between an assistant tool-call and the tool
  message answering it, producing 400 "No tool call found for function call
  output with call_id ...". The reverse pairing is left alone: a call awaiting
  its result is what a suspended approval looks like.
- ignore.ts called statFs without importing it, so walk() crashed on the first
  symlink.

482 tests, up from 404. Docs synced across README, ROADMAP, TODO, and all of
docs/: tool sets, the new tools, ctrl-c semantics, the tool-start event, and the
two hand-maintained tool-name lists recorded as a known weakness.
2026-09-03 01:37:48 +07:00

4.2 KiB

Headless mode

-p runs one prompt without the TUI. For scripts, CI, and piping.

shiro -p "list every route and its handler"
git diff | shiro -p "review this diff" --yolo
shiro -p "fix the failing test" --yolo --agent deep

The prompt comes from the argument, or from stdin when the argument is omitted.

Tool use needs --yolo

There is no terminal to approve on, so every gated tool is denied unless --yolo is passed:

$ shiro -p "add a test for paginate()"
shiro: headless denies write_file, edit_file, multi_edit, bash and mcp tools unless --yolo is passed
[tool] write_file {"path":"test/paginate.test.ts",...}
[denied] write_file (run with --yolo to allow tool use in headless mode)

Read-only tools work either way, so -p without --yolo is a safe way to ask questions about a codebase from a script. That includes read_many_files, list_dir, and the git tools, which is enough to review a diff or explain a module without any write access at all.

--yolo does not disable plugin guards. rm -rf is still refused.

Output

Text mode (default)

Assistant text to stdout, everything else to stderr. Pipe-friendly:

shiro -p "one-line summary of src/session.ts" > summary.txt
$ shiro -p "what does prune.ts do?" 2>/dev/null
src/prune.ts repairs provider-item dependencies after pruneMessages strips reasoning items.

JSON mode

--json emits one event per line:

$ shiro -p "count the tools" --json
{"type":"tool-start","id":"c1","name":"grep"}
{"type":"tool-call","id":"c1","name":"grep","input":{"pattern":"tool\\("}}
{"type":"tool-result","id":"c1","name":"grep","output":"src/tools.ts:26: ..."}
{"type":"text","text":"There are 14 built-in tools."}
{"type":"done","inputTokens":4210,"outputTokens":88}

Event types: text, reasoning, tool-start, tool-call, tool-output, tool-result, tool-error, tool-denied, compacted, notice, error, done.

tool-start arrives before the arguments have finished streaming, so it carries the name but no input. Use tool-call when you need the arguments.

Errors are flattened to message strings, because JSON.stringify turns an Error into {} and a JSON stream that reports failures as empty objects is useless for the one case it matters.

Exit codes

0 on success, 1 on a model or stream error. A denied tool is not a failure — the model was told and can respond to it.

if shiro -p "does this build?" --yolo; then echo ok; else echo failed; fi

Sessions

Headless runs save like interactive ones, so -c picks up where one left off:

shiro -p "start the refactor" --yolo
shiro -p "now update the tests" --yolo -c

What is withheld

The ask tool is not offered at all, rather than being offered and left to hang. The model is told to decide and state its assumption instead.

Subagent progress events are not emitted; the report still comes back.

There is no terminal, so ctrl-c cannot interrupt a single command the way it does interactively — a signal kills the run. Cap the risk with the timeout the model passes to bash, or with --agent quick to cap the step count.

CI recipes

Review a pull request diff:

- run: |
    git diff origin/main...HEAD > /tmp/diff
    shiro -p "Review this diff. Report defects with file and line. Say so if it is clean." \
      --agent review < /tmp/diff

--agent review is read-only, so no --yolo is needed and nothing can be modified.

Fail the build on a specific finding:

- run: |
    shiro -p "Does any handler skip input validation? Answer only YES or NO." --json \
      | jq -r 'select(.type=="text") | .text' | grep -qv YES

Generate a changelog entry:

- run: |
    git log --oneline "$(git describe --tags --abbrev=0)"..HEAD \
      | shiro -p "Write a changelog entry from these commits. Group by user-facing change." \
      >> CHANGELOG.md

Pass the key as a secret:

env:
  OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}

Cost control

Headless runs are unattended, so a runaway loop costs real money. --agent quick caps the step count at 12, and { "toolSets": [] } trims the schema sent every request. There is no spend ceiling yet — see TODO.md.