Packs & configuration
Packs bundle hooks you install once and get everywhere. Exactly two providers exist — the capt-hook wheel’s builtins and Claude Code plugins — with zero per-repo config: no manifest, no enable list, no .claude/capt-hook.toml. This page covers using the packs you have, shipping your own in a plugin, and tuning hook settings.
Builtin packs activate themselves
capt-hook ships seven builtin packs. Five are always active in every repo:
general— language-agnostic guards: git/jj command safety, a session guard that deniespkill/killall, akillorreniceof a pid unless this session can prove it started the process, since ancestry alone does not establish ownership, the harness’sTaskStoptool from a subagent or Orca lane, since a bare task id does not tell a disposable shell task from a workflow, agent, or teammate session (the root session stops any of its tasks unchecked),orca terminal close/worktree rm/worker-stop(the root coordinating a dispatch’s Orca Run may stop that dispatch with one literalworker-stop),launchctlteardown, session-endingosascript/softwareupdate/pmset/tmux, andshutdown/reboot, in Bash and in MCP payloads, doc and prompt nudges, task and plan discipline, model-routing nudges, a code-review gate, a docs-freshness gate, a detour check-in nudge, a tooling nudge that asks for a tool-fix lane after a first-party tool refuses, a manual workaround next to a tool, or a command repeated by hand, verbose-comment enforcement (a new over-budget comment is denied; an edit to a comment that was already over budget only warns),rmsafety, and a plain-English reply rewrite. An owner ruling that names its target lifts a terminal close, launchd service stop, a lane’sTaskStop, or akillthrough tickets for as long as the ruling stands, and each close, service-stop, or kill ticket covers an identical retry for two minutes. A lifted Bash call must run once at top level, first in the payload, with only pipe successors. A session may also close an idle Orca terminal it proves it created itself. The rewrite sends each assistant reply through Cerebras (qwen-3.8-27b) and does nothing untilCEREBRAS_API_KEYis set. With the key set, it hides the reply’s streamed chunks and shows the rewrite in their place once the last chunk arrives. Code blocks, names, numbers, and commands carry over unchanged. A reply with under 200 characters of prose outside code blocks shows as written, and a failed or empty rewrite shows the original reply and records a fault.fixes— workarounds for upstream Claude Code issues: auto-approving a teammate’s permission dialogs and scratch-file writes when the session ran with--dangerously-skip-permissions.steering— judgment nudges: prefer first-principles fixes over band-aids, don’t dismiss a real pre-existing issue, don’t chase trivial type noise, don’t swap the requested fix for docs.performance— wall-clock pipelining nudges that speculate a held fix while verification runs, plus a subprocess resource monitor. One host ticker incapt-hookdsamples the children of each registered session. When a child runs pastmin_runtime_secondsat sustained CPU or disk pressure, the monitor warns the session through its next hook event and a desktop notification, then waits out the grace period. It then asks TypeSafe Jev whether the child is disposable. When Jev says yes with at least 0.9 probability, the monitor sends one targetedSIGTERMafter it revalidates the pid, parent, process group, start time, command, working directory, and session ownership. Deploys, releases, publishes, database clients and migrators, and anything protected are never signaled, and neither are their ancestors. Protected means agent sessions, terminal hosts, PTY daemons, Orca, launchd, and the Captain Hook host. Jev cannot override those exclusions. See Tune the subprocess resource monitor to change the thresholds or turn the monitor off. The pack also refuses a new local Go or Cargo build or test while the host is saturated; see Defer local builds on a saturated host.graphite— Graphite (gt) workflow guards that stay inert outside agt repo init’d repo. Every hook checks for.git/.graphite_repo_configacross worktrees and honorsccx.nogt. That git-config key opts a repo out of the gt lane, so a stale marker cannot activate the pack. Neither can a marker on a repo Graphite has no access to: with ccx on PATH the pack readsccx vcs lane --jsonand stands down wherever ccx reports a lane other thangt, five-minute cached per repository so a linked worktree and its main checkout read one verdict. One hook refuses a command; the rest note or rewrite. A jj write gets a note saying the repository’s stack lives in Graphite; read-onlyjjcommands (log,status, diff,bookmark list, …) get none when everyjjcall on the line is a read. Rawgit commit/git push/branch writes get a note namingccx vcs ship/ccx vcs stack new. Its presubmit review reminder quiets once a cc-review pass has run or the session’s dispatch prompt contains a wholeccx: role=fixline. Anyrolevalue counts because the parent owns the lane’s review pass. Rebase, merge, and pull notes nameccx vcs stack rebase(ccx 0.65.0 or later) orccx vcs stack submit. When ccx is on PATH, a stack write with a ccx twin is rewritten to it:gt submit,gt s, andgt ssbecomeccx vcs stack submit(-d/--draftcarries over as--draft), and a baregt restackbecomesccx vcs stack restack. Agit push --force-with-leaseof the checked-out branch toorigin, with no lease value of its own, becomesccx vcs push, which force-pushes under a lease only when the branch was rewritten and refuses a remote head the branch never held. The rewrite applies only when the ccx verb covers every flag on the call and no wrapper, env prefix, or global option would be dropped. Everything else it routes, fromgt sync,gt create,gt modify, a scopedgt restack, andgit rebaseto a plaingit push --force, runs as written with a note naming the ccx command. A rebase control (--continue,--abort,--skip) inside a ccx conflict workspace runs as written too, with a note namingccx vcs stack continue/abort(ccx 0.65.0 or later), since it is the workaround when that verb fails. End a command with a real shell comment,# ccx:raw, or setCAPT_HOOK_CCX_RAW=1for the session, to run it as written and silence the note. Quoted text and heredoc bodies do not count as comments. Only1,true, oryesenable the environment escape. Matching ignores case and surrounding whitespace;0does not count. With ccx on PATH, agh pr view --jsonquery for landing fields warns and namesccx vcs pr status <n>(ccx 0.64.1 or later) orccx vcs status; the warning quiets after either command runs this session. The one refusal is a push to a PR the Graphite merge queue holds. With ccx on PATH,git push,ccx vcs push,ccx vcs ship,ccx vcs stack submit, andgt submitlook up each pushed branch’s open PR and read their queue state in oneccx vcs pr status --jsoncall. A queued PR that receives any head but the commit the queue admitted is refused. The queue lands that commit and drops the rest, so the refusal namesccx vcs stack new <name>andccx vcs shipto put the change in a stacked PR instead. When the queued PR is only a parent that accx vcs shipwould push along with its downstack, the refusal namesccx vcs ship --tip-onlyinstead, which pushes only the shipped branch.# ccx:raworCAPT_HOOK_CCX_RAW=1pushes anyway, and a lookup that fails, times out, or answers from a stale cache allows the push.
Two more activate by language, detected from the repo’s build manifests:
go— a narrowest-test-run nudge and ago.modnudge — active when a recursive, non-ignoredgo.modorgo.workexists anywhere in the repo. The test nudge stands down while the host is saturated, so it never asks for a test theperformancepack refuses to start.python— a narrowest-test-run nudge, AST style rules, and auvmissing-dependency nudge — active when a recursive, non-ignoredpyproject.tomlexists.
Detection walks the repo once, pruning VCS metadata and .gitignored directories, so a vendored go.mod under .venv or a testdata pyproject.toml you ignore never counts. There is no opt-out for a builtin: to change what fires, fix the policy in capt-hook itself rather than adding a repo exception. To turn every capt-hook behavior off, disable the captain-hook plugin — its wiring dispatches every event, so without it no hook runs, builtin or plugin.
List the active packs
pack list shows the active builtins plus any pack your enabled plugins ship — read-only, no config to edit:
uvx capt-hook pack list builtin:fixes builtin 2 hooks
builtin:general builtin 13 hooks
builtin:graphite builtin 1 hooks
builtin:performance builtin 3 hooks
builtin:python builtin 3 hooks
builtin:steering builtin 3 hooks
plugin:cc-context@cc-context plugin 32 hooks
# ...plus a row per pack your other enabled plugins ship
A pack’s runtime id is builtin:<name> for a wheel builtin and plugin:<plugin-id> for a plugin pack. A plugin pack’s version, description, and repository all derive from its plugin.json — nothing about a pack is separately authored.
Test your own hooks
capt-hook test runs the inline tests on your repo’s own .claude/hooks/ modules:
uvx capt-hook testThe builtins are tested by captain-hook itself, and a plugin’s pack is tested from its own repo with pack test — so capt-hook test covers only what your repo owns.
How packs combine with your own hooks
Your .claude/hooks/ modules import first, so they register ahead of every pack. A block from any hook wins the event outright; among approvals the first registered wins, so a local approval outranks a pack’s on the same event. Packs are the shared baseline; your local hooks stay authoritative.
Ship a pack in a plugin
A Claude plugin can carry one capt-hook pack, and dispatch discovers it: every event, the dispatcher enumerates the project’s enabled plugins and loads the pack any of them ships at the fixed path capt-hook/{pack.toml, hooks/} under the plugin root. The pack applies to whatever project the plugin is enabled in, with zero repo edits and zero per-session wiring — the plugin runs nothing itself.
There is no other way to distribute a pack: no GitHub sources, no tarballs, no pins, no per-repo enable list. A standalone pack becomes a minimal plugin.
The layout
A pack plugin ships its pack at one fixed path:
your-plugin/
├── .claude-plugin/
│ └── plugin.json
└── capt-hook/
├── pack.toml
└── hooks/
├── guard.py
└── ...
The capt-hook/ directory is the pack. Present, it must carry both a pack.toml and a hooks/ directory; a capt-hook/ missing either, or with a malformed pack.toml, is a fatal load error at every dispatch in every repo that enables the plugin — never a silent skip, because a dropped pack silently drops guards. A plugin with no capt-hook/ directory merely consumes packs and is skipped. Wire pack test (below) into the plugin’s CI so a malformed pack is caught before release.
pack.toml is a tiny descriptor
The descriptor holds only what can’t be derived from the plugin — its identity, version, description, and repository all come from plugin.json. So pack.toml carries just two things, both optional:
# Resources the pack's hooks need provisioned before they run.
resources = ["spacy:en_core_web_sm", "wordnet:oewn:2025"]
# MCP tool gate semantics: how a tool lowers to a built-in gate.
[tools.ccx_code_edit]
behaves_like = "Edit"
span_edit = { path = "path", content = "content", delete = "delete" }
[tools.BashFormat]
behaves_like = "Bash"resources names NLP or tool resources the pack’s hooks need eagerly provisioned; capt-hook provisions the union of every active pack’s resources once at session start. A [tools.<name>] table tells capt-hook an MCP tool behaves like a built-in gate — its key is the bare tool segment (ccx_code_edit, not mcp__server__ccx_code_edit), which matches the tool under any server or mount prefix. A tool name may be claimed by only one pack; a collision across packs is fatal. A pack that needs neither still ships a pack.toml (an empty resources = [] is fine) — the file is what declares capt-hook/ an intentional pack.
The captain-hook dependency
plugin.json must declare the captain-hook dependency with a version floor:
{
"dependencies": [
{ "name": "captain-hook", "marketplace": "captain-hook", "version": ">=11.0.0" }
]
}The object form is required: name, marketplace, and a >=X.Y.Z floor. This dependency is what pulls the dispatcher onto a machine that installs only your plugin; without it the pack has no runtime.
Test the pack
pack test validates and runs a working-tree pack, so CI needs one line:
uvx capt-hook pack test .Pass the plugin root — the directory holding .claude-plugin/plugin.json and capt-hook/. It requires all three artifacts, validates the captain-hook dependency floor, validates the pack.toml resources and tool specs, loads only this working-tree pack, and runs its inline tests. It exits non-zero on a missing layout, a bad dependency, an unknown resource, a load error, zero hooks, an async_=True decision hook, or a failing test.
Tell users how to install
Two lines — your marketplace and your plugin:
claude plugin marketplace add your-org/your-repo
claude plugin install your-plugin@your-marketplaceOn a machine that has never seen captain-hook, installing just your plugin surfaces Claude Code’s dependency error: the plugin.json dependency points at a marketplace the machine doesn’t know, so the plugin lands disabled with dependency-unsatisfied. The one-time fix is registering the marketplace — claude plugin marketplace add yasyf/captain-hook, or capt-hook init in any repo, which registers it as a side effect. From then on the dependency resolves and Claude Code installs the dispatcher itself.
How discovery works
Dispatch reads the project’s plugin roster from Claude Code’s own files on every discovery, with no subprocess. ~/.claude/plugins/installed_plugins.json lists each install’s path, scope, and, for a project or local install, the project it belongs to. The enabledPlugins maps in the user, project, local, and managed settings files decide which plugins are on, with the higher-precedence file winning. In a git worktree the local settings file is the one at the main checkout’s root, as in Claude Code. Discovery then checks each enabled install’s root for the fixed capt-hook/ path. Installing, enabling, or disabling a plugin takes effect on the next event, and so does editing a hook file inside a plugin.
A plugin can be installed at more than one scope. Discovery ignores installs that belong to another project, skips install paths that no longer exist, and follows Claude Code’s precedence among the rest: local over project over user. Two different install paths at the same scope of one project mean a corrupt roster, and so does an installed_plugins.json that isn’t a roster object. Either way discovery loads zero plugin packs and records the error where capt-hook status and the next session start show it.
Policy that arrives outside these files, such as an MDM plist, a remote managed policy, or per-session flags like --plugin-dir and --settings, is not visible to discovery.
Discovered packs are first-class: they show in uvx capt-hook pack list as kind plugin. Their inline tests run from the plugin’s own repo with pack test, not from a consuming project’s capt-hook test.
Route a misfire home
When the session reviewer mines a misfiring hook, it routes the fix PR to the pack’s home repo: a builtin pack’s fix goes to captain-hook, and a plugin pack’s fix goes to the repository in its plugin.json. A plugin that declares no repository has no route, so its misfires are dropped rather than misfiled.
Configure hook settings
You define a HooksSettings subclass in .claude/hooks/conf.py, override its fields from code or environment variables, and read the values inside a handler through evt.ctx.c.
Define a settings class
Create .claude/hooks/conf.py and subclass HooksSettings. Add your own fields with type annotations and defaults:
from captain_hook import HooksSettings
class ProjectSettings(HooksSettings):
test_command: str = "pytest"
require_tests_after_edit: bool = TrueThe loader discovers conf.py when it imports your hooks package, finds the HooksSettings subclass, and instantiates it for every event. You do not call any builder yourself. Put exactly one subclass in conf.py so the loader has a single class to pick.
Read settings in a handler
Every event carries the active settings on evt.ctx.c. Cast it to your subclass to get typed field access:
from typing import cast
from captain_hook import Event, Tool, on
from .conf import ProjectSettings
@on(Event.PreToolUse, only_if=[Tool("Bash")])
def enforce_test_command(evt):
settings = cast(ProjectSettings, evt.ctx.c)
if (cmd := evt.command).raw and cmd.q.runs("pytest") and settings.test_command != "pytest":
return evt.block(f"Run the project test command: {settings.test_command}")
return NoneOverride fields from the environment
HooksSettings reads environment variables with the HOOKS_ prefix, so each field maps to one variable. Set HOOKS_TEST_COMMAND=mtest to override test_command without touching conf.py. List and tuple fields take JSON:
HOOKS_PLANNING_AGENTS='["Explore","Plan","research"]'To namespace project settings under a different prefix, set model_config on your subclass:
from pydantic_settings import SettingsConfigDict
from captain_hook import HooksSettings
class ProjectSettings(HooksSettings):
model_config = SettingsConfigDict(env_prefix="MYAPP_")
require_tests_after_edit: bool = TrueNow MYAPP_REQUIRE_TESTS_AFTER_EDIT=false flips the field, and every inherited field moves under the new prefix too. Pick the prefix once at the project root and keep it stable.
Tune the built-in fields
HooksSettings ships built-in fields you can override the same way.
planning_agents
A list of subagent names that the framework treats as scratch work, not real edits. skip_planning_agents is on by default for every hook, so hooks ignore Event.SubagentStart and Event.SubagentStop for any agent on this list and planning runs do not trip your gates. The defaults cover Claude Code’s built-in planning agents, such as Explore and Plan. Add your own when your team uses custom planning agent types:
class ProjectSettings(HooksSettings):
planning_agents: list[str] = [
*HooksSettings.model_fields["planning_agents"].default_factory(),
"research",
]waiting_tools
A list of tool names that mean the agent is waiting on something external, not working. The Waiting() condition reads this list to suppress nudges and gates while a background tool is still running. The defaults cover Claude Code’s long-running tools, such as Monitor and TeamCreate. Append your custom async tools to the defaults:
class ProjectSettings(HooksSettings):
waiting_tools: list[str] = [
*HooksSettings.model_fields["waiting_tools"].default_factory(),
"MyAsyncTask",
]state_dir
Where @session_state and @workflow_state models persist their JSON. The field reads CAPTAIN_HOOK_STATE_DIR, falling back to ~/.claude/state. Set CAPTAIN_HOOK_STATE_DIR to relocate state, which helps in CI sandboxes that block writes outside the workspace. See State and sessions for how state directories are laid out.
log_dir
Where the framework writes its own session logs. The field reads CAPTAIN_HOOK_LOG_DIR and otherwise falls back to ~/.cache/captain-hook/logs, or to $XDG_CACHE_HOME/captain-hook/logs when you set XDG_CACHE_HOME.
Tune the subprocess resource monitor
The performance pack’s resource monitor reads PerformanceSettings, which uses the HOOKS_PERFORMANCE_ prefix. The defaults live in internal/wireproto/resource.json, so the Go host and the Python worker read one set of numbers.
The host reads the first ten keys below and the worker reads all fourteen. Where each value comes from differs by key:
- Per session: the host takes the thresholds, the grace period, the escalation delay, and the per-session cap from the environment of the request that registers the session. Set those in the shell that launches Claude Code.
- Per daemon: the sample interval and the registry cap come from the environment
capt-hookdstarted with. The interval also sets how many samples fill the sustain window. A session’s ownHOOKS_PERFORMANCE_SAMPLE_INTERVAL_SECONDSdoes not change either one. - Worker: the worker reads its keys from the environment of each hook run, so they follow the session.
| Key | Default | Valid range | Read by | Meaning |
|---|---|---|---|---|
HOOKS_PERFORMANCE_ENABLED |
true |
true or false |
host and worker | Turns the monitor on or off |
HOOKS_PERFORMANCE_SAMPLE_INTERVAL_SECONDS |
15 |
1 to 86400 | host, daemon | Seconds between ticks of the shared sampler |
HOOKS_PERFORMANCE_MIN_RUNTIME_SECONDS |
120 |
1 to 86400 | host | A child must have run this long before it can raise pressure |
HOOKS_PERFORMANCE_CPU_FRACTION |
0.5 |
above 0, up to 1 | host | CPU use that counts as heavy, as a fraction of one core |
HOOKS_PERFORMANCE_SUSTAIN_SECONDS |
60 |
1 to 86400 | host | How long CPU or disk use must stay heavy before the warning |
HOOKS_PERFORMANCE_DISK_BYTES_PER_SECOND |
52428800 |
0 or more | host | Disk throughput that counts as heavy, 50 MiB per second |
HOOKS_PERFORMANCE_GRACE_SECONDS |
60 |
1 to 86400 | host | Wait between the warning and the judge |
HOOKS_PERFORMANCE_ESCALATE_AFTER_SECONDS |
30 |
0 to 86400 | host | Wait after SIGTERM before the escalation stage; 0 disables escalation |
HOOKS_PERFORMANCE_MAX_TRACKED_PER_SESSION |
32 |
1 or more | host | Children tracked at once per session |
HOOKS_PERFORMANCE_REGISTRY_CAP |
512 |
1 or more | host, daemon | Sessions the host monitors at once |
HOOKS_PERFORMANCE_TERMINATE |
true |
true or false |
worker | Whether a disposable verdict ends in a signal |
HOOKS_PERFORMANCE_JUDGE_TIMEOUT_SECONDS |
3 |
1 to 300 | worker | Time limit for one Jev call |
HOOKS_PERFORMANCE_MAX_JUDGE_CALLS_PER_SESSION |
10 |
1 or more | worker | Judge calls allowed per session |
An invalid value does not stop the session. If the daemon’s own environment holds one, the daemon logs the error and runs with the defaults. If a session’s environment holds one, the host logs the error and does not monitor that session. In the worker, an invalid value makes the stage refuse.
To keep the warnings and never stop a process, set termination off:
export HOOKS_PERFORMANCE_TERMINATE=falseThe warning then says termination is off, and the judge stage leaves the child running. To switch the monitor off entirely, set HOOKS_PERFORMANCE_ENABLED=false.
What stops a signal
The monitor signals a child only when every check passes. Each of these conditions leaves the process running instead:
- The stage has no current qualifying reading, or the process table, the child’s row, or the child’s working directory is unreadable. A child with no readable working directory is never signaled, and a child whose current readings are all unknown is not signaled at any stage, escalation included.
- A sample that cannot be read empties the window for that metric. Pressure must then be proven again with fresh readable samples before the child advances.
- The command line, or an ancestor’s, matches an exclusion, or the child or one of its ancestors is protected.
- Jev times out, rejects the request, has no key, declines the question, or rates the child disposable below 0.9, or the session runs out of judge calls.
- The event payload is malformed, or the session state on disk is corrupt.
- The host cannot dispatch the stage: it is shed, it expired or was abandoned past its deadline, or the host is closing.
- The registry is full, or the session’s settings are invalid, so the session is never monitored.
Only the warn stage waits for an acknowledgment. The warn hook answers proceed, and the host stops tracking the child when the answer is anything else. The judge and escalate stages are fire and forget from the host’s side: the host does not read the reply. Every refusal in the worker records a notice that the next hook event delivers to the session instead.
The host also refuses a ResourcePressure event sent by a client, and capt-hook run ResourcePressure refuses it on the cold path, so only the host raises the event. The host derives the session’s anchor from the authenticated hook process rather than from the parent pid the request claims.
Escalation sends SIGKILL only after the worker recorded its own SIGTERM for the same process identity. It then repeats the full ownership and exclusion checks and revalidates the identity before it signals. The host runs the escalation stage once per child.
Limits
- The judge sees the daemon’s environment. The model command-line client inherits the daemon worker’s environment, minus the keys the hooks whitelist. A secret exported into the daemon’s own environment is visible to it. The judge prompt carries a redacted command line, and the redaction is a best effort.
- macOS validation and signal are separate calls. The monitor checks the process, then sends the signal with
kill(2). In the milliseconds between them the process can exit and its pid be reused,execa different program, or change directory. The monitor reads the working directory first and the identity last to narrow that window, but it cannot close it. - Linux uses
pidfd. Signaling through a pidfd rules out pid reuse. Anexecor a directory change after validation can still happen. - The worker’s last identity check is command text, not the argv vector. The host tracks a child by its pid and sub-second start time at every tick and reads the argv vector it sends fresh at each stage dispatch. Right before a signal the worker checks only what
psexposes, which is the parent and group ids, the whole-second start time, the working directory, and the command text with whitespace collapsed. Two argument vectors that join to the same text pass that check, and a process that runsexecwith such arguments during the judge passes with a verdict about its earlier invocation. - Other users’ processes are not signaled. The sampler rows carry no user id, and Linux can expose a process’s CPU use across users. The worker refuses any target whose user id differs from the session’s.
- Disk pressure can be unknown. On Linux, a process whose
/proc/<pid>/iothe monitor cannot read has no disk reading, so only its CPU counts. - Children that predate registration are never tracked. A session registers on its first hook event. Anything already running then stays out of scope. Only processes that first appear afterward and descend from the session’s
claudeprocess are tracked. - Reparented children can drop out. A process is identified by its pid and start time, so reparenting does not make a new identity. A descendant that leaves the session’s process subtree is dropped. The own children of a child the monitor stopped are tracked only if they first appeared after registration and still descend from the session.
- A calm child keeps its warning. A child that drops below the thresholds is not warned a second time for the same run.
Defer local builds on a saturated host
The host counts as saturated when its 1-minute load average is at or above HOOKS_ADMISSION_LOAD_PER_CORE times its core count. The default is 1, so a 32-core host is saturated at a load of 32. The worker reads the key from each hook run’s environment.
While the host is saturated, the performance pack refuses a Bash call that starts any of these on the local machine:
go build,go test,go vet, includinggo -C <dir> <verb>go run golang.org/x/tools/cmd/deadcodecargo build,cargo check,cargo test, includingcargo +<toolchain> <verb>
The check walks the parsed command line. It finds these calls by an absolute executable path, behind env, timeout, nice, time, command, exec, or xargs, inside a sh -c or eval payload, and in a command substitution. A command ssh sends to another host is not parsed as a call, so ssh forge go build ./... and remote-builder exec --lane go -- go test ./... both run. ssh forge true && go build ./... is still refused, because its go build runs locally. The refusal names the load and core count and tells the agent to let CI grade the commit. Remote work the task already has approval for stays possible when written explicitly.
The go pack’s test nudge skips under the same condition.
Limits
- It sees only the command line. A build started by
make, a script,mise exec,uv run,gtimeout, ordocker runis not recognized. Neither is one whose executable name comes from a variable. - A routing shim reads as local. A plain
go testorcargo buildthat a wrapper onPATHforwards to a remote builder is refused like any local one. Write the remote form explicitly instead. - It is a threshold, not a lease. Several calls admitted in the same moment can all start, and a build already running is not stopped.
- Cargo global options hide the verb.
cargo --locked checkis not recognized, because only a+<toolchain>argument may come before the verb.
See also
- How hooks run explains the dispatch pipeline and the verdict precedence behind pack ordering.
- State & workflows covers where
@session_stateand@workflow_statedata lands understate_dir. - Configure settings example is the tested, runnable version of the settings handler.