Skip to content

CLI reference

There is one command to learn: sr. sr <group> <cmd> is a proxy for the sr-<group> binary (sr session start runs sr-session start), passing stdio, signals and exit codes through unchanged. The plugin’s hooks call the sr-<group> binaries directly; you use sr. You rarely call any of this by hand — the hooks run it — but this is the whole surface, generated from the binaries’ own command definitions.

sr-session

Session lifecycle — the hook points a harness calls.

Enforce a project's declared guardrails at the moments a harness can pause. A project declares guardrails under .sloprail/guardrails/; the harness calls the hook points below, and the engine runs whichever guardrails bind to what is about to happen. sr-session start a session is beginning sr-session pre-tool a tool is about to run — the refusable moment sr-session stop a turn has ended sr-session subagent-stop a subagent's turn has ended sr-session id | query | state what a hook asks about the session so far sr-session trajectory ... read a trajectory — describe it, cite into it There is no setup command. The guardrails directory is created by whatever writes the first declaration, and a project with none is an ordinary project — the engine loads cleanly and no session sees a difference. The hook subcommands are invoked by a harness with a payload on stdin, not typed by a person. The event kinds a guardrail may bind to are per-build, and `sr-session start` reports them: a declaration binding to a kind this build does not produce is refused by name, and the refusal lists every kind it does produce. To write a guardrail, use the authoring-guardrails skill.

sr-session id

The identity this conversation keeps, whatever id the harness now reports.

sr-session pre-tool

Before a tool call: run the guardrails bound to what it would do.

sr-session query

What the agent did — the part of the session not yet judged.

What the agent did — the part of the session not yet judged. Reads from where the last completed cycle stopped. A session's record only grows, so a turn already judged has not changed, and judging it again both wastes the reading and invites a judge — a model call, not a function — to reach a different verdict on a turn the agent can no longer reach to fix. The position moves only when a cycle finishes. A cycle that was interrupted may have judged nothing, so the next one reads those turns again: re-reading a turn costs a second look, skipping one loses a violation for good. The first cycle of a session, and any session whose position cannot be read, gets the whole record for the same reason. --whole-session ignores the position and reads the entire record, for a rule asking about the session as a whole rather than about this cycle's work. Entries belonging to sub-agents are left out unless asked for: a rule asking what the agent did usually means the main line of work. The answer is entries as JSON, for whatever the hook already uses to read JSON.

FlagDefaultMeaning
--include-sidechainsfalseInclude entries belonging to sub-agents
--where—Narrow which entries come back, over the same expression language a guardrail's matcher uses
--whole-sessionfalseRead the entire record, not just the part no cycle has judged yet

sr-session start

Session start: record the baseline and report the project's declarations.

sr-session state

What this guardrail remembers within this session.

What this guardrail remembers within this session. A rule spanning more than one cycle needs somewhere to keep what it knows: a refactor declared before it happens and reconciled after, a trigger seen in one cycle and answered in the next. None of that fits in an event. Neither the guardrail nor the session is an argument. Both come from the environment the engine sets when it runs a hook — SR_GUARDRAIL, SR_SESSION_ID, SR_WORKSPACE — because a hook able to name either could read a rule it was never told about, or reach into another session. Outside a hook there is no guardrail in scope, and these commands say so rather than guessing which rule is asking.

  • sr-session state get — Read back what this guardrail stored under a key
  • sr-session state list — The entries a guardrail stored under a prefix (this one, or --owner's)
  • sr-session state set — Store a value under a key, replacing what was there

sr-session stop

End of a cycle: run the guardrails bound to what it changed.

sr-session subagent-stop

End of a SUB-AGENT's cycle: run the guardrails bound to what it changed.

sr-session trajectory

Read a trajectory — facts about it, a citation into it, its normalized entries.

Read a trajectory: the record of one session's turns. sr-session trajectory describe facts about a trajectory — whether it is a sub-agent's, its parent, the sub-agents it spawned sr-session trajectory cite turn a substring of the user's own words into a resolvable <path>:<line> citation (--include-envelope also prints the whole AskUserQuestion answer envelope at that line) sr-session trajectory tool-result whether a cited --line is a tool_result, and its content — for a delivery observation that names a transcript line as proof of work describe answers "what IS this file"; cite mints a citation an agent can write, and with --include-envelope reads back the question+answers at that citation's line in the same call; tool-result confirms a line an observation names is a real tool-call result. All default to the trajectory the hook was invoked for, and all accept --path to read another — the parent or a sibling that describe named.

  • sr-session trajectory cite — Turn a substring of the user's own words into a <path>:<line> citation
  • sr-session trajectory describe — Facts about a trajectory — is it a sub-agent's, its parent, its sub-agents
  • sr-session trajectory normalize — The trajectory as normalized entries, each carrying the events re-derived from it
  • sr-session trajectory tool-result — Report whether a cited line is a tool_result, and print its content

sr-file

Check a file's contents, or change a file grounded in citations.

Check a file's contents, or change a file grounded in citations. EXIT STATUS, for every subcommand: 0 success; 1 the input or the change was refused (each subcommand's help says what its 1 means, and `field` adds 2 and 3); 64 a usage error — an unknown command or flag, a wrong argument count, a flag with no value, --as missing, misapplied or naming no format, an unknown --cite pool.

sr-file declarations

Load and validate the .sloprail declarations under a directory.

Load and validate the new-format declarations a project keeps under its .sloprail directory — file-guards, gates, contexts, and the structure gate — reporting what loaded and, for anything that did not, the faults by name. This is an inspection surface over the declaration loader, not part of the live hook dispatch: it proves a project's declaration files parse and validate before any event is dispatched against them. THE ARGUMENT is a directory. A path ending in .sloprail is loaded as the root itself; any other path is treated as a project root whose .sloprail subdirectory is loaded — so `declarations .` and `declarations ./.sloprail` name the same tree. --plugin NAME loads the directory as the root of an installed plugin named NAME instead of a project, validating what a plugin ships by the plugin rules — a plugin's structure gate must declare a `scope`, a project's must not. STRUCTURE GATES are listed one per root: the project's (the whole tree) and each plugin's with the scope it owns. EXIT STATUS is 0 when every declaration loaded and 1 when any was invalid, so a hook or a CI step can read it. Invalid declarations are printed one fault per line. EXAMPLES: sr-file declarations . sr-file declarations examples/eval-loop-maxing sr-file declarations examples/eval-loop-maxing/.sloprail sr-file declarations --plugin mdmap path/to/plugins/mdmap

FlagDefaultMeaning
--plugin—load the directory as the root of the plugin with this name, not a project

sr-file delete

Delete a file, grounded in cited words.

Delete a file. It must exist and be a regular file. Citations: --cite:<source-types> <quote>, repeatable. <source-types> is user (the user's own words), tool_result (a tool's output), or both comma-separated. Each quote must resolve to exactly one entry of the session's trajectory, or nothing is written. The words must match exactly; whitespace need not (a line break in the message matches a space in the quote). Quote with single quotes so the shell leaves it verbatim. tool_result also resolves in the records of the sub-agents the session dispatched, so a sub-agent can cite its own tools' output. user resolves only in the user's messages in the main conversation: a sub-agent's prompt is the parent agent's, so a sub-agent quotes the user's words exactly as the user wrote them, and a parent dispatching work that must cite the user pastes the user's exact words into the prompt. Flags take their value as the next word or after '='. '--' ends the flags.

sr-file edit

Replace an exact string in a file, grounded in cited words.

Replace --old-string with --new-string — the Edit tool's semantics. --old-string must occur exactly once unless --replace-all is given, and the file must exist. Citations: --cite:<source-types> <quote>, repeatable. <source-types> is user (the user's own words), tool_result (a tool's output), or both comma-separated. Each quote must resolve to exactly one entry of the session's trajectory, or nothing is written. The words must match exactly; whitespace need not (a line break in the message matches a space in the quote). Quote with single quotes so the shell leaves it verbatim. tool_result also resolves in the records of the sub-agents the session dispatched, so a sub-agent can cite its own tools' output. user resolves only in the user's messages in the main conversation: a sub-agent's prompt is the parent agent's, so a sub-agent quotes the user's words exactly as the user wrote them, and a parent dispatching work that must cite the user pastes the user's exact words into the prompt. Flags take their value as the next word or after '='. '--' ends the flags.

sr-file field

Print one top-level field of a document, read as plain YAML.

Print one top-level field of a document, read as plain YAML — no schema. WHICH BYTES ARE THE DOCUMENT is decided as `validate` decides it: the frontmatter of a .md, the whole of a .yaml, .yml or .json; '-' reads stdin, with --as. The value is printed when it is a scalar (aliases and merge keys followed); an absent field, or one whose value is a list or a map, prints nothing. Exit status: 0 the document was read (the value, or nothing, is on stdout) 1 the document cannot be read: it does not parse as YAML, holds more than one YAML document, or defines the field (or a merge key) twice; or the input is not a regular file, or is larger than the cap 2 the file has no frontmatter: no opening --- fence 3 an opening --- fence is never closed: a forgotten close, or a rule above prose (the caller reads the text after it to tell which) 64 a usage error: an unknown command, flag or argument count EXAMPLES: sr-file field memories/topics/a/units/01/UNIT.md status jq -r .event.newContent payload.json | sr-file field - status --as .md

FlagDefaultMeaning
--as—How to read bytes on stdin: .md, .yaml or .json. Required with '-', and refused with a path

sr-file validate

Check a file against a CUE schema.

Check a file against a CUE schema. WHICH BYTES ARE THE DOCUMENT is decided by the file's extension: the frontmatter of a .md, the whole of a .yaml, .yml or .json. A markdown file is a YAML document wearing prose below it, so handing the whole file to a schema checker gets an error about the prose. An extension outside that set is an error rather than a guess. THE SCHEMA IS CUE, unmodified — the same file `cue vet` would take. Exit status is 0 when the document satisfies the schema and 1 when it does not, so a hook can read it. Failures name the file, the field, and what was expected, one line per problem, and every problem is reported rather than only the first. BYTES ON STDIN, with '-' as the path, for a hook holding content that is not on disk: at a Pre event the write has not happened, so the pending bytes are in the event and the file either does not exist or still holds the old ones. --as says how to read them, and is required, because a pipe carries no name to take the format from. --emit prints the validated document as JSON on success, so a caller can take a field out of it with jq instead of parsing the same bytes a second time. Nothing is printed for a document that failed. A path must be a regular file (a FIFO or device is refused rather than read), and input — a file or stdin — larger than 8 MiB is refused: a document this checks is frontmatter or a config file, and reading without bound would let one input stall the hook. EXAMPLES: sr-file validate memories/note.md --schema .sloprail/schemas/note.cue sr-file validate config.yaml --schema schema.cue --path '#Config' jq -r .event.fields.newContent event.json | sr-file validate - --as .md --schema s.cue sr-file validate DECISION.md --schema s.cue --emit | jq -r .transcript_path

FlagDefaultMeaning
--as—How to read bytes on stdin: .md, .yaml or .json. Required with '-', and refused with a path
--concrete, -ctrueRequire all fields to be concrete — a missing required field fails (cue vet's -c)
--emitfalseOn success, print the validated document as JSON on stdout
--path, -d—Schema definition to check against, e.g. '#Config' (cue vet's -d)
--schema, -s—The CUE schema to check against (required)

sr-file write

Write a file's whole content, grounded in cited words.

Write a file, replacing whatever it held — the Write tool's semantics. The content is --content, or stdin when --content is absent (a heredoc: sr-file write notes.md --cite:user 'keep a changelog' <<'EOF' ... EOF). Citations: --cite:<source-types> <quote>, repeatable. <source-types> is user (the user's own words), tool_result (a tool's output), or both comma-separated. Each quote must resolve to exactly one entry of the session's trajectory, or nothing is written. The words must match exactly; whitespace need not (a line break in the message matches a space in the quote). Quote with single quotes so the shell leaves it verbatim. tool_result also resolves in the records of the sub-agents the session dispatched, so a sub-agent can cite its own tools' output. user resolves only in the user's messages in the main conversation: a sub-agent's prompt is the parent agent's, so a sub-agent quotes the user's words exactly as the user wrote them, and a parent dispatching work that must cite the user pastes the user's exact words into the prompt. Flags take their value as the next word or after '='. '--' ends the flags.

sr-mark

Write // sr:<kind> <fqn> enforcement markers into impl files.

sr-mark apply

Write // sr:<kind> <fqn> markers into impl files.

Write `// sr:<kind> <fqn>` enforcement markers into impl source files. KIND is the first argument and is free-form — the marker vocabulary is the project's, not this tool's, so any kind works with no code change. It must be alphanumeric, and may use `:` to namespace itself (e.g. `blueprint:test`), `_`, `-` and `.`. Each marker pair is a flag whose NAME is the invariant fqn and whose value is <path>:<line> — i.e. `--<fqn>=<path>:<line>`. Pass many pairs in one call to write many markers. An fqn may use any character a URL admits, but must contain NO whitespace: a marker is one line of text, so a name with a space in it would write cleanly and then be unreadable — the marker reader is line-anchored and would not match it at all. Paths are resolved against --root (default $SLOPRAIL_GIT_ROOT, else cwd); line is 1-based. Writing is idempotent and uses the right comment leader for the file's language. All pairs are validated before any file is written. EXAMPLES: sr-mark apply blueprint --user.email_unique=src/auth/user.ts:42 sr-mark apply blueprint:test --user.email_unique=src/auth/user_test.ts:9 sr-mark apply docs --order.Cart.total=src/order.ts:13 --user.email_unique=src/user.ts:42

sr-mark delete

Remove // sr:<kind> <fqn> marker comments for the given fqns, wherever found.

Scan the impl tree (--root, default $SLOPRAIL_GIT_ROOT or cwd) for `// sr:<kind> <fqn>` (or `# …` / `-- …` for SQL) comments naming any of the given fqns, and remove each matching comment LINE. A marker that also carries other text on the same line is left untouched (the scanner only matches lines that ARE a marker comment, mirroring how sr-mark writes them on their own line). Silently no-ops for an fqn with no marker anywhere. KIND is the first argument and is free-form, same as for `apply`; the fqns follow. EXAMPLE: sr-mark delete blueprint order.cancel_only_pending order.cancel_only_from_early_status

sr-agent

Run an agent, whichever harness is running.

Run an agent without naming the harness that will run it. The prompt is positional, as every harness takes it. --prompt is accepted instead, for when the prompt comes from a file or a pipe and a positional would be awkward. sr-agent --model size-md "does this uphold the invariant?" sr-agent --model claude-opus-5,size-lg --prompt "$(cat question.txt)" MODEL SETS --model takes preferences in order, first match wins. Two kinds of entry: size-xs size-sm size-md size-lg size-xl size-xxl A size, not a model. Every harness maps every one of them, so an alias ALWAYS resolves — which means anything after an alias in a set can never be reached. Writing size-md,claude-opus-5 probably meant the other order. claude-opus-5, sonnet, ... One harness's own model. Matches only under that harness, so a set may name several harnesses' models and mean "the best available here". A set matching nothing under the running harness is REFUSED, not quietly run on a default. A verdict from a model the author did not choose is one nobody can account for. HARNESS Which harness is running is read from the environment, not asked for. An environment naming no known harness is refused rather than guessed at; pass --harness for a hook running outside any harness at all.