コンテンツにスキップ

Writing a Pipeline

このコンテンツはまだ日本語訳がありません。

FileScopeNotes
rtk:default, rtk:posthook, rtk:proxy, rtk:delegate-reads, rtk:verbatim-diffs, rtk:tersebuilt inread-only; compiled into the binary — see Reference Pipelines
~/.config/rtk/pipeline.tomlyouloads first
~/.config/rtk/pipelines/*.tomlyouevery file loads, alphabetically — name them 10-…, 20-… if order matters
.rtk/pipeline.tomlthis projecttrust-gated — rtk init in the project shows what it contains and asks; a changed file asks again
.rtk/pipelines/*.tomlthis projectevery file loads, alphabetically; each trust-gated
~/.config/rtk/filters.toml, .rtk/filters.tomlyou · projectyour custom command filters, unchanged — see Methods

Paths use the Linux spelling. The user directory is the platform config dir: ~/.config/rtk/ on Linux, ~/Library/Application Support/rtk/ on macOS, %APPDATA%\rtk\ on Windows.

Load order: built-ins → your pipeline.toml → your pipelines/ → the project’s pipeline.toml → the project’s pipelines/. Later wins. One file is the simple case; a directory is how you keep several pipelines and turn them on by dropping them in.

Across files: definitions ([task], [upstream], [method]) merge by key; [override] applies later-wins per field; a rule or directive name that appears in two loaded files is an error (E013) naming both paths — nothing shadows silently, and rtk pipeline show prints the source file of every line. Files do not extend each other: they are all loaded, so to reuse a piece, give it its own file.

proxy.request rules belong in your user files: one daemon serves every project, so it never reads a project’s .rtk/ pipeline (W017).

config.toml is unchanged and separate: preferences ([tracking], [display], [hooks], [retriever], [telemetry], [limits], [awareness]) stay there. The pipeline holds behaviour; it never replaces an existing RTK file.

schema_version = 1
[pipeline] # name, extends, enabled
[[hook.before_tool.<subject>]] # rules — before a tool runs
[[hook.after_tool.<subject>]] # rules — after a tool ran
[[proxy.request.<part>]] # rules — the request, about to be sent
[[directive]] # what RTK tells the agent
[method.<name>] # chains, options for built-in methods
[classifier.<name>] # AI judges rules can condition on
[task.<name>] # delegation commands
[upstream.<name>] # model endpoints
[override.<name>] # patch an inherited rule or directive
[guards] # global safety limits

A minimal file uses one or two of these.

KeyValue
nameoptional for your files; rtk:* for built-ins
extendsa built-in name or a list of them: "rtk:default", ["rtk:delegate-reads", "rtk:terse"]. Left to right; a shared ancestor is inherited once; the same rule name from two parents is an error unless the rules are identical. Built-ins only — local files compose by being loaded, not by extending each other. Omit for a standalone file.
enabledfalse turns the whole file off without moving it. Default true.

A rule’s position says three things, in this order:

SegmentAnswersValues
sitewhich RTK mechanism runs ithook · proxy
eventwhenbefore_tool · after_tool · request
subjecton whata tool category (hook) · a part of the request (proxy)

The position fixes what the rule can see and do. file_lines exists before a tool runs (the file is on disk); format exists after (there is output). A rule that names a condition its position cannot provide does not load — see Conditions and Judges.

You write the category; RTK maps your agent’s tool names to it.

SubjectMeaningClaude Code tools it covers
readread a fileRead, NotebookRead, TaskOutput
writecreate or overwrite a fileWrite
editmodify a fileEdit, MultiEdit, NotebookEdit
shellrun a shell commandBash, PowerShell, Monitor
searchsearch files or contentGrep, Glob, LSP
webfetch or search the webWebFetch, WebSearch
subagentspawn or message a sub-agentTask, Agent, SendMessage, ListAgents, Workflow
mcpany MCP toolmcp__*, ToolSearch, WaitForMcpServers, ListMcpResourcesTool, ReadMcpResourceTool
anyfallback — evaluated only if no rule under the specific subject matchedall

Other agents are mapped the same way; rtk pipeline explain shows which category a call was assigned, and Agent Coverage lists what each agent supports.

PartWhat the rule sees
tool_resultone tool-result block in the conversation
tool_useone tool-call block (the agent’s request, including a written file’s body)
assistantone assistant message
cachecache breakpoint placement
anyany block; fallback

What the proxy actually runs at these parts is on What the Proxy Compresses.

If you think in the layers Claude Code’s /context shows, this is where each is configured. Layers are how rtk gain --by-layer reports; they are never written in the file.

LayerWhere
Tool resultshook.after_tool.{shell,search,web,subagent,mcp} (rtk:posthook) · proxy.request.tool_result
Readhook.before_tool.read · hook.after_tool.read
Writeproxy.request.tool_use · the code-write task
Historyproxy.request.tool_result · proxy.request.assistant
Cacheproxy.request.cache
Agent messagedirective terse-output
Skills / MCP · User promptdirectives
Inferenceroute to a task before any tool, or after any tool with extractable output, optionally gated on a judge
[[hook.after_tool.shell]]
name = "term-default"
when = { is_rtk_command = false }
use = ["term"]
continue = false
enabled = true
shadow = false
KeyRequiredNotes
nameyes*unique across the resolved pipeline. *Defaults to the first use entry when that is unambiguous; a derived name that collides with another rule is a warning — set it explicitly
whennoconditions, all must hold; omitted = matches everything
use / actionexactly oneuse is a chain of methods; action is control flow (below)
task · upstream · messagewith route / denysee Actions
modenoroute before a tool only: suggest (default) · swap · answer — see Route modes
continuenodefault false — first match wins; true keeps evaluating later rules
enablednodefault true; a disabled rule is skipped but still shown by rtk pipeline show
shadownotransforms only; decide and record, emit nothing — see Shadow mode

Within one [[site.event.subject]] list: the project’s rules first, then yours, then inherited rules — within a scope, files in load order, then declaration order. First match wins unless continue = true. The any list is evaluated only after the specific subject’s list matched nothing.

rtk pipeline validate warns when a rule can never fire — for instance a rule after one with no when and no continue in the same list (W001).

What a rule may do depends on its position:

Positionuse = [...]action =Extra keys
hook.before_tool.shell—rewrite · deny · passthrough · routetask with route; message optional
hook.before_tool.read—deny · passthrough · routetask with route; message optional
hook.before_tool.write—deny (warned: the body is already generated; a deny only buys a retry) · passthrough · routetask with route
hook.before_tool.edit—deny · passthrough
hook.before_tool.{search,web,subagent,mcp,any}—deny · passthrough · routetask with route
hook.after_tool.{shell,search,web,mcp,subagent,any}transform chainpassthrough · routetask with route
hook.after_tool.readtransform chainpassthrough · route (W021: breaks a later Edit)task with route
hook.after_tool.{write,edit}— (no output to extract, W013)passthrough
proxy.request.{tool_result,tool_use,assistant,any}transform chain — cache-stable methods only—
proxy.request.cache["cache-align"]—

rewrite — prefix the command with rtk when RTK has a filter for it (ls → rtk ls); leave it untouched otherwise. It never takes a target or options. Its settings ([hooks] exclude_commands, transparent_prefixes) stay in config.toml.

deny — refuse the call. message is shown to the agent; ${file_path}, ${file_lines}, ${command} interpolate. A deny can only depend on fixed conditions, never on a judge (E020).

passthrough — stop here, do nothing. Before a tool: exempt the command from rewrite. After: skip compression for this output.

route + task — hand the work to a cheaper model (Tasks and Upstreams). Where the rule sits decides how.

The call has not run, and no agent lets a hook return a result, so RTK denies the call with a generated message naming the task — “Read of src/Service.java (4,120 lines) is blocked. Run: rtk bulk-read --question "<what you need to know>" src/Service.java”, or for a command “npm test is handled by a worker model. Run: rtk check-tests --question "<what you need to know>" -- npm test” — and the agent runs it. The agent supplies the question because only the agent knows why it wanted the file or the command. Override the text with message. A routed command still exits with the command’s own code.

What each call is handed over as, and the task kind that takes it (a mismatch is W023):

BeforeHanded over asTask kind
shellthe command: -- <command>digest
readthe file pathsummarize or digest
web-- curl -sL <url>digest
search-- rg -n '<pattern>' <path>digest
write--spec … --reference … --target <file>generate
subagentits description as the question, over files the agent namessummarize
mcp, anya question onlya plugin kind

Gate it on a judge to delegate only the calls worth it.

By default a route before a tool suggests the task, and the agent spends a turn running it. mode lets the rule deliver the cheaper answer in the same turn instead:

modeWhat happensExtra turnWorks on
suggest (default)the call is refused with a message naming the task; the agent decides to run ityesevery subject, every agent
swapthe command is replaced by the task wrapping it — rtk <task> --question "…" -- sh -c '<command>' — and runs as the agent’s own callnoshell
answerRTK runs the task itself and gives the agent the answer in place of the callnoread, search, web
[upstream.haiku]
provider = "anthropic"
model = "claude-haiku-4-5"
[task.bulk-read]
kind = "summarize"
upstream = "haiku"
context = true
[task.check-build]
kind = "digest"
upstream = "haiku"
context = true
[[hook.before_tool.read]]
name = "answer-big-reads"
when = { file_lines_gt = 400, whole_file = true }
action = "route"
task = "bulk-read"
mode = "answer"
[[hook.before_tool.shell]]
name = "swap-builds"
when = { command_matches = "^mvn " }
action = "route"
task = "check-build"
mode = "swap"

With swap and answer there is no agent writing the question, so RTK asks one. With context = true on the task, it is built from what the agent is doing — the user’s request, the agent’s current step and its recent calls. Without it, a generic question asks for what matters, keeping exact names, numbers and error lines.

  • swap needs nothing from the hook: the agent’s own call runs the task, and the command keeps its exit code. If the worker fails, the command’s own output is shown. Agents that cannot replace a command show the swapped command as a suggestion instead.
  • answer waits for the worker inside the hook, bounded by the upstream’s timeout_secs — set it low. If anything fails (no key, timeout, empty answer), the original call runs as if the rule had not matched. After an answered read the agent can still read exact lines with offset and limit, since a ranged read is not whole_file.
  • Any other combination is refused (E005): swap is for commands only, and answer on a shell command is what swap does without blocking the hook.

On any subject with extractable output the output already exists, so there is nothing to ask the agent to re-run: RTK sends the output to the task and puts the answer in its place, with the original kept for rtk recall. The task must be a digest instance. Set context = true on it so the worker knows what the agent was looking for; otherwise it answers a generic question.

This is the one rule that makes a provider call on the hook path — bound it with the upstream’s timeout_secs (default 30 s; set it lower for the hook path). Every failure — no upstream, refused, timed out, empty answer — passes the original output through untouched.

route after a read is allowed but warned (W021): a later Edit needs the exact bytes, so keep it to files the agent only reads. After write and edit there is no output to replace (W013).

Everything you inherit has a name. [override.<name>] patches one inherited rule or directive in place; fields you do not mention are kept.

[pipeline]
extends = ["rtk:delegate-reads", "rtk:posthook"]
[override.shunt-large-read]
when.file_lines_gt = 500
[override.strip-line-numbers]
enabled = true
[override.term-default]
use = ["ansi", "dedup"]
[override.rtk-awareness]
level = "full"
FieldSemantics
enabled action task upstream message continue shadow and module optionsreplaced
when.<key> = vpatches one key; the rest of when is kept
when = { … }replaces the whole condition — validate warns and lists the keys you dropped (W003)
usereplaced whole
name, position in the treenot overridable — that is a new rule

Overriding a name that does not exist (E011), or declaring a new rule with an inherited name (E012), does not load. Project overrides beat user overrides beat built-ins. rtk pipeline show prints where each value came from.

Add vs change: to change how an inherited rule behaves, override it. To add behaviour, write a new rule with a new name — it is evaluated before the inherited ones and can shadow them for what it matches.