Agentic Workshop
Module 09 · 60 min

Non-interactive CLI, JSON, and pipelines

Reliable automation doesn't depend on sentences to interpret. It depends on stable inputs, schemas, exit codes, and a failure path that prevents subsequent steps from acting on incomplete results.

Lessons

What you will tackle, step by step

9.1

Interactive and non-interactive

When you are in front of the terminal you can correct a request on the fly. A pipeline can't raise their hand and ask you what you meant. This is why the transition to non-interactive mode does not consist of removing the chat: it consists of making explicit everything you previously decided during the conversation.

In an interactive session, the user can clarify a request halfway. In a pipeline, inputs and conditions must be complete before starting. The script needs to know when to exit, what to print, and what status to return.

Use small, versioned inputs: for example `git diff main | claude -p "Review this diff" --output-format json`. Pin the working directory and add timeout, allowed tools, and exit code management to the wrapper. In CI, `--bare` avoids loading unexpected local configurations. Don't depend on invisible conversations or the implicit shell folder.

See it in practice · Transform a manual review into a repeatable command

Every evening you want to analyze the changes of Task Notes without depending on the chat history or the folder the shell happens to be in.

  1. Sets the repository with an explicit path and identifies the range Git to examine, for example from base commit to `HEAD`.
  2. Pass objective and constraints in a versioned instruction file instead of reconstructing them from previous messages.
  3. Define timeout, expected output and exit code for success, finding and technical error.
  4. Run twice on the same commit and compare essential artifacts before handing over to a scheduler.

Result: The review no longer depends on an invisible session. Anyone with the same commit and contract can replay the execution and figure out where it stopped.

9.2

JSON as contract, not decoration

Putting curly braces around an answer does not turn it into a contract. JSON becomes useful when you know which fields exist, what type they have, and what should happen if they are missing. Otherwise you just have more uncomfortable prose to read.

Structured output is useful when fields are defined and validated. With `--json-schema`, the schema-compliant result is returned in the `structured_output` field; free text nested without a contract only shifts the problem of interpretation.

The `stream-json` format outputs the protocol events of Claude Code during execution. Your program must recognize those real types and can then transform them into application events such as progress or finding; should not assume that the stream produces custom names invented by the prompt.

See it in practice · Shape a report that a script can use

The pipeline should block the next stage when the review finds a high error, but continue if only informational notes exist.

  1. Define `status` as an enum and `findings` as a list of objects with `severity`, `file`, `message` and possible `line`.
  2. Make `runId` and `schemaVersion` mandatory, so you can attribute output and handle format changes.
  3. Validate the JSON before reading the findings; if validation fails, classify the execution as a technical error.
  4. Calculate the gate from validated fields, not searching for words like "critical" in free messages.

Result: The pipeline distinguishes review content and format errors. A new sentence in the message does not accidentally change the automatic decision.

9.3

Gate and failure propagation

A gate that turns red but lets work through is not a gate: it's a decoration. The hard part of a pipeline is not starting lint and tests, but propagating the failure so that no subsequent stage can behave as if everything was fine.

A reliable pipeline validates the output before using it, stops the path when lint or test fails, and propagates a non-zero exit code. However, generating an error report does not mean continuing with deployment or publication.

Preserve the artifacts useful for diagnosis: reduced log, responsible phase and validated output. `claude -p` uses the exit code for the success or technical failure of the execution; a valid JSON with a high finding can still arrive with exit code 0. It is the wrapper that must read that result and transform it into the exit code decided for your gate.

  1. Fix the routine's input, working directory, and time limit.
  2. Request structured output and validate it against the schema.
  3. Run lint and test as gate with meaningful exit codes.
  4. Abort dependent phases at first failed gate.
  5. Preserve the report, responsible phase, and artifacts useful for diagnosis.
See it in practice · Prevent an incomplete report from reaching the publication stage

The review produces a valid JSON, but the linter finds an error. The pipeline must save the diagnosis and not execute the command that prepares the release.

  1. Perform schema validation and stop immediately if the output does not meet the contract.
  2. Run the linter, capturing output and exit code without automatically transforming the error into success.
  3. If the exit code is non-zero, save phase, command, version and useful portion of the log in a local artifact.
  4. Return a non-zero code from the main process and verify that the release step is skipped, not failed after startup.

Result: Failure remains diagnosable and confined. The pipeline clearly communicates that the release never started, instead of leaving an ambiguous intermediate state.

9.4

Auto mode and operating range

Skipping confirmations may seem like a small optimization. It actually shifts the boundary of authority: a misinterpretation no longer pauses for your review. Before narrowing the checkpoints, you need to reduce the scope of the environment, tools, and consequences.

The permission modes are not equivalent: `default` asks for sensitive actions, `acceptEdits` reduces confirmations on changes, `plan` remains read, `dontAsk` only allows what you have pre-approved, while `auto` uses background security checks. Availability and behavior depend on version and plan.

`bypassPermissions`, equivalent to `--dangerously-skip-permissions`, skips checks and is only appropriate in isolated environments. Don't disable protections globally because a pipeline is noisy: limit folders, tools, network, and duration, and keep separate approvals before external effects.

See it in practice · Run an automated review inside a controlled sandbox

You want to run a nightly review of Task Notes without approving every read, but the computer also has credentials for the remote repository.

  1. Create a dedicated working copy and limit the routine to read plus write of the local report only.
  2. Remove unnecessary push tokens and logins from session; disable networking if sources are already local.
  3. Set timeout, maximum number of files and immediate stop when faced with commands outside the expected list.
  4. Run multiple times with active log; only then evaluate which repetitive confirmations can be reduced without expanding authority.

Result: The routine gains autonomy without becoming omnipotent. Even if it misinterprets a file, it cannot publish, delete, or leave the assigned folder.

Guided example

Local review pipeline

Task Notes must check every change without publishing anything.

1. Read the modified files
2. Generate {status, findings, tests}
3. Validate the JSON schema
4. Run lint; if it fails → exit 1
5. Run tests; if they fail → exit 1
6. Save the report locally; no deployment