Files & Commands
File
A file path wrapper with glob matching, prefix checks, and test-file detection.
Usage
File(
*,
path,
)Delegates Path methods via __getattr__ so .suffix, .name, .parent, .exists() etc. work directly.
Parameter Attributes
path: Path
file.PathMatcher
A reusable set of glob patterns for matching file paths. Supports in operator.
Usage
file.PathMatcher(*, patterns)Parameter Attributes
patterns: list[str]
file.categorize_files()
Split paths into source, test, and skipped buckets for a language.
Usage
file.categorize_files(paths, *, lang="py")A path that does not match the lang globs is skipped; otherwise it is classified as a test file (via File.is_test, which treats conftest.py and anything under a tests/ directory as tests) or as source.
Parameters
paths: Iterable[str | Path]-
File paths to categorize; blank entries are ignored.
lang: str = "py"-
Language key into
LANG_GLOBS(defaults to"py"); unknown keys fall back to*.<lang>.
Returns
list[str]-
A
path strings.(source, test, skipped)tuple, each a sorted, de-duplicated list of
util.fs.read_json()
Read and parse a JSON file, returning default on missing file or parse error.
Usage
util.fs.read_json(path: Path) -> dict[str, Any] | None
util.fs.read_json(path: Path, default: dict[str, Any]) -> dict[str, Any]util.fs.resolve_binary()
Resolve an absolute, executable path for name, or None.
Usage
util.fs.resolve_binary(name, *, extra_dirs=())Search order: $CLAUDE_PLUGIN_ROOT/bin/<name>, then each extra_dirs entry’s <name>, then shutil.which(name).
util.fs.binary_supports()
Whether the binary name advertises flag in its --help output.
Usage
util.fs.binary_supports(name, flag)Resolves name through resolve_binary(), probes <path> --help with a 2s timeout, and tests flag for token membership in the combined stdout/stderr — splitting on runs of non-flag characters, so a bracketed [--foo] or a --foo=VALUE form still matches. A missing binary, a probe error, or a timeout yields False. Memoized for 600s, so a hot path pays the probe at most once per window.
Cmd
The parsed command line behind evt.cmd — a walk over every command invocation it contains.
Usage
Cmd(
line,
raw="",
cwd=None,
event=None,
bindings=dict(),
replacements=dict(),
notes=list()
)evt.cmd is always a Cmd (an empty line yields zero calls), never None, so a handler never guards before iterating. The walk consumes the parser’s payload parts: nested sh -c/eval payloads and command substitutions surface as calls of their own (three levels deep), and cd threads the working directory to the calls after it within the same payload scope. Construct one detached for scanning untrusted payloads: Cmd(command_line) over an already-parsed line, or Cmd.parse(text) (None when the text is too deeply nested to parse). A detached Cmd has no bound event, so Call.sub() raises on it.
raw is the true original command text as written: str(cmd) returns it, and it is the operand for raw-text matching — the ~captain_hook.conditions.Command regex and ast_grep search it, so a line that parses to zero commands (a comment, a shebang) still matches by its text. bool(cmd) follows raw, staying truthy for such a line even when line is falsy. q and line expose the parsed structure.
Parameter Attributes
line: CommandLineraw: str = ""cwd: Path | None = Noneevent: ToolRewriteEvent | None = Nonebindings: Mapping[str, Binding] = dict()replacements: dict[int, str] = dict()notes: list[str] = list()
Example
>>> if (call := evt.cmd.call("rm")) and len(call.targets.expand()) > 10:
... return call.sub("rm", "trash")Attributes
| Name | Description |
|---|---|
| q |
The fluent query over the parsed line — CommandLine.q.
|
| scope |
The same-line variable bindings of the whole command text, over bindings in force before it.
|
q
The fluent query over the parsed line — CommandLine.q.
q: CommandLineQuery
scope
The same-line variable bindings of the whole command text, over bindings in force before it.
scope: Scope
Methods
| Name | Description |
|---|---|
| call() | The first command invocation named name, or None. |
| calls() | Every command invocation in the line (nested payloads included), or those named name. |
| parse() |
Parse text into a detached Cmd, or None when it is too deeply nested to parse.
|
| scope_for() |
The scope word of call resolves in, and the word’s offset within that scope’s text.
|
call()
The first command invocation named name, or None.
Usage
call(name)calls()
Every command invocation in the line (nested payloads included), or those named name.
Usage
calls(name=None)parse()
Parse text into a detached Cmd, or None when it is too deeply nested to parse.
Usage
parse(text)scope_for()
The scope word of call resolves in, and the word’s offset within that scope’s text.
Usage
scope_for(call, word)Call
One command invocation reached by walking a command line — a top-level occurrence, a
Usage
Call(cmd, occurrence, cwd)nested sh -c/eval payload, or a command substitution.
command is the cc-transcript Command with leading wrappers (sudo, env, timeout, …) stripped arity-aware; name is the dequoted head word’s basename, casefolded ("rm" -rf / names the call rm), so matching is command-position-only by construction (echo rm foo and git rm never match "rm") and quoting the executable never evades it.
Parameter Attributes
cmd: Cmdoccurrence: Occurrencecwd: Path | None
Example
>>> for call in evt.cmd.calls("rm"):
... if call.targets.expand().exhausted:
... return evt.block("targets too broad to verify")
... return call.sub("rm", "trash", args=call.targets) or evt.block("unrecoverable rm")Attributes
| Name | Description |
|---|---|
| args | The unwrapped command’s arguments, with shell quoting removed. |
| command | The command with leading wrappers stripped. |
| flags |
The option tokens, dequoted, with any registered value-flag’s argument (git -C <dir>) alongside it.
|
| leading_options | The global options written before the verb, each registered value flag with its argument. |
| name | The unwrapped head word’s basename, dequoted and casefolded. |
| nested | Whether this call sits below top level — a payload or substitution hop away. |
| redirects | The command’s file redirects. |
| source | The command as written, wrappers included. |
| spliceable | Whether a rewrite can be spliced back — the source command carries a byte span. |
| substituted |
Whether a command substitution ($(...), backticks) feeds this call’s words.
|
| targets | The operand tokens as Target objects, value-flag arguments and options removed. |
| verb_argv | name plus the arguments from the verb on, leading_options dropped. |
| wrappers |
The leading wrapper commands stripped to reach this call, e.g. ("sudo",).
|
args
The unwrapped command’s arguments, with shell quoting removed.
args: tuple[str, …]
command
The command with leading wrappers stripped.
command: Command
flags
The option tokens, dequoted, with any registered value-flag’s argument (git -C <dir>) alongside it.
flags: tuple[str, …]
leading_options
The global options written before the verb, each registered value flag with its argument.
leading_options: tuple[str, …]
git -C <dir> -c x=y push yields ("-C", "<dir>", "-c", "x=y"). An informational option (INFO_OPTIONS) ends the run, since git --help stash runs no stash.
name
The unwrapped head word’s basename, dequoted and casefolded.
name: str
nested
Whether this call sits below top level — a payload or substitution hop away.
nested: bool
redirects
The command’s file redirects.
redirects: tuple[Redirect, …]
source
The command as written, wrappers included.
source: Command
spliceable
Whether a rewrite can be spliced back — the source command carries a byte span.
spliceable: bool
Substitution payloads, fish -c payloads, and joined eval payloads carry none. A nested payload with a span splices through its quote layers when the replacement survives them, which sub() checks per rewrite.
substituted
Whether a command substitution ($(...), backticks) feeds this call’s words.
substituted: bool
targets
The operand tokens as Target objects, value-flag arguments and options removed.
targets: Targets
An operand with a same-line expansion the call can resolve() contributes one target per candidate value; one it cannot stays unverified.
verb_argv
name plus the arguments from the verb on, leading_options dropped.
verb_argv: tuple[str, …]
git -C <dir> push and git -c x=y push both read ("git", "push"), while git --help stash keeps its --help and so never reads as a stash.
wrappers
The leading wrapper commands stripped to reach this call, e.g. ("sudo",).
wrappers: tuple[str, …]
Identified by basename against the authoritative WRAPPER_COMMANDS table. A wrapper value-flag argument that itself names a wrapper (sudo -u env rm) can appear here, but a membership check against a real wrapper head is unaffected.
Methods
| Name | Description |
|---|---|
| resolve() | Resolve one of this call’s words through the assignments written earlier on the same line. |
| resolved_targets() | words as targets, each same-line expansion the call can resolve() expanded to its candidates. |
| sub() |
Rewrite this call’s old executable to new in place, returning the rewrite result.
|
resolve()
Resolve one of this call’s words through the assignments written earlier on the same line.
Usage
resolve(word)A literal word resolves to itself. A word carrying $NAME or ${NAME} expansions resolves when every name was set by a literal, unconditional, top-level assignment or for head before the word (HOME reads as ~); a nested payload sees the bindings in force where it began, a single-quoted shell payload only the exported ones. Anything else — a value built by a command substitution, a conditional or looped assignment, a read, a positional parameter, a word inside a function body, or a word with no byte span — is ~captain_hook.bindings.Unresolved, carrying the text its value is built from when the line shows it.
resolved_targets()
Usage
resolved_targets(words)sub()
Rewrite this call’s old executable to new in place, returning the rewrite result.
Usage
sub(old, new, *, args=None, note=None)old must equal name (else ValueError — a programming error, not a runtime condition). Returns None when the call is not spliceable, is substituted (its operands cannot be re-emitted faithfully), or the replacement cannot survive the enclosing quote layers — so a handler composes the fail-closed fallback in policy code: call.sub("rm", trash) or evt.block(...).
The occurrence’s span is replaced with new followed by the re-emitted arguments — each argument’s verbatim source spelling (Word.raw). args defaults to the call’s own arguments; pass args=call.targets to drop flags and keep only operands, in which case a --leading raw gains a ./ prefix so new never misparses it as a flag. Leading wrappers drop (sudo rm /x → trash /x); redirects outside the span survive.
Subs accumulate on the parent Cmd: each call returns a fresh rewrite splicing every sub so far, so returning the last applies them all (rm a && rm b rewrites both) and returning evt.block(...) instead discards them all — a line never rewrites partially. A Cmd that is detached or bound to a non-rewrite event raises RuntimeError.
CommandSchema
Bind parsed shell words using declarative option and positional definitions.
Usage
CommandSchema(
program,
operands=(),
options=(),
options_end_operands=False,
operands_end_options=False,
separators=()
)Schemas operate on an existing Call and never parse shell text. With options_end_operands, the first non-prefix option starts a trailing expression; expression arguments never become positional operands. With operands_end_options, the first operand ends option reading the way POSIX utilities such as kill do, so a later option word is unknown and kill 123 -l never reads as a listing. Unknown options end binding and mark the result incomplete instead of guessing their arity. A value attached to a short option (-oVAL) binds only when every single-dash alias is one letter, since a schema with find-style single-dash words cannot split such a token unambiguously.
Parameter Attributes
Methods
| Name | Description |
|---|---|
| bind() | Bind an invocation while preserving the parser’s word provenance. |
bind()
Bind an invocation while preserving the parser’s word provenance.
Usage
bind(call)Operand
Bind positional words to a role, optionally collecting a variable-length group.
Usage
Operand(name, count=1, default=())A variadic operand reserves words for the fixed operands after it. Defaults are literal words used only when no explicit word binds the role.
Parameter Attributes
name: strcount: int | Literal["*"] = 1default: tuple[str, …] = ()
Option
Bind option aliases to a named, typed value or a flag.
Usage
Option(name, flags, type=str, prefix=False, until=(), first=False)prefix keeps positional binding open for leading options in a schema whose options start a trailing expression. until consumes an opaque argument list through its terminator, such as a nested command passed to another program. first binds the option only as the first word after the program, the way kill reads a signal spec: it never joins a flag cluster, and a later spelling is an unknown option that ends binding incomplete, so kill -9 -15 cannot read its negative process group as a second signal.
Parameter Attributes
name: strflags: tuple[str, …]type: type[str] | type[int] | type[bool] = strprefix: bool = Falseuntil: tuple[tuple[str, …], …] = ()first: bool = False
Arguments
Named bindings from one invocation, retaining source words and unknown values.
Usage
Arguments(words, values, cwd, complete, operands_complete, call, unread=())complete is false when an unknown option, missing value, invalid typed value, or command substitution prevents a complete binding. Known path operands remain available so a policy can still reject an explicitly broad root. unread holds the words from the first unknown option on, or the option left without its value, so a policy can name exactly what stopped the binding.
Parameter Attributes
words: dict[str, tuple[Word, …]]values: dict[str, tuple[Scalar | None, …]]cwd: Path | Nonecomplete: booloperands_complete: boolcall: Callunread: tuple[Word, …] = ()
Methods
| Name | Description |
|---|---|
| paths() | Return the named role as path targets with the invocation’s working directory. |
paths()
Return the named role as path targets with the invocation’s working directory.
Usage
paths(name)A word with a same-line expansion the call can resolve contributes one target per candidate value; one it cannot stays unverified.
CommandMatches
Apply argument predicates to each matching invocation, including nested calls.
Usage
CommandMatches(schema, only_if=(), skip_if=())Every only_if predicate must match the same invocation. Any skip_if predicate exempts that invocation alone, so a safe sibling cannot hide a match.
Parameter Attributes
schema: CommandSchemaonly_if: tuple[Callable[[Arguments], bool], …] = ()skip_if: tuple[Callable[[Arguments], bool], …] = ()
PathsMatch
Match when any path in a named argument role satisfies a path predicate.
Usage
PathsMatch(name, predicate)Parameter Attributes
name: strpredicate: PathMatches
PathMatches
Match complete path globs against lexical and resolved targets.
Usage
PathMatches(patterns, unresolved=False)* matches one path segment; ** can cross directories. Patterns may use ~ for the current home. Unknown shell expansions match only when explicitly requested through unresolved; no shell variables are evaluated or replaced.
Parameter Attributes
patterns: tuple[str, …]unresolved: bool = False
OptionIs
Match a completely parsed invocation’s last value for a named option.
Usage
OptionIs(name, values)Parameter Attributes
name: strvalues: Collection[Scalar]
Target
One operand of a command — a literal path or a glob — with resolution and blast-radius predicates.
Usage
Target(value, raw, cwd)value is the operand with shell quoting removed, or None when an unresolved expansion ($VAR, $(...)) taints the word; such a target is not verified and can never be statically classified — every predicate returns False and expand() reports itself exhausted, so a guard falls through to its fail-closed branch instead of approving. A glob or tilde operand (*.py, ~/x) keeps its value and stays verified: bash expands it, but to statically known candidates.
path resolves the parent directory but keeps the final component literal: a symlink is classified as itself, never followed through, which is what a deletion or command target needs (act on the entity named, not what it points at). This is deliberately unlike the ScratchPath condition, which fully resolves — write-approval must see through a symlink to where the bytes actually land. Do not unify the two.
Every predicate returns False when path is None (an unverified target, or a relative target with no known cwd).
Parameter Attributes
value: str | Noneraw: strcwd: Path | None
Example
>>> target.is_repo_root or target.contains_repoAttributes
| Name | Description |
|---|---|
| contains_repo | Whether the target is a directory that contains a git/jj repository. |
| has_glob | Whether the target’s value contains shell glob metacharacters. |
| in_repo | Whether the target lives inside a git/jj repository. |
| is_fs_root |
Whether the target is the filesystem root /.
|
| is_home |
Whether the target is a home directory (~ or a top-level /Users entry).
|
| is_repo_root | Whether the target is itself a git/jj repository root. |
| is_scratch | Whether the target sits under a temp root or a scratch-named ancestor directory. |
| path |
The target resolved against cwd, parent-resolved with the final component kept literal.
|
| verified | Whether the operand is statically known — False when an expansion taints it. |
contains_repo
Whether the target is a directory that contains a git/jj repository.
contains_repo: bool
has_glob
Whether the target’s value contains shell glob metacharacters.
has_glob: bool
in_repo
Whether the target lives inside a git/jj repository.
in_repo: bool
is_fs_root
Whether the target is the filesystem root /.
is_fs_root: bool
is_home
Whether the target is a home directory (~ or a top-level /Users entry).
is_home: bool
is_repo_root
Whether the target is itself a git/jj repository root.
is_repo_root: bool
is_scratch
Whether the target sits under a temp root or a scratch-named ancestor directory.
is_scratch: bool
path
The target resolved against cwd, parent-resolved with the final component kept literal.
path: Path | None
verified
Whether the operand is statically known — False when an expansion taints it.
verified: bool
Methods
| Name | Description |
|---|---|
| expand() |
Expand a glob target to its matches (capped at limit + 1); a literal yields itself.
|
expand()
Expand a glob target to its matches (capped at limit + 1); a literal yields itself.
Usage
expand(*, limit=GLOB_LIMIT)An unverified target expands to nothing, exhausted — it cannot be verified.
Targets
The operand targets of a command, in order — iterable, sized, and expandable as a whole.
Usage
Targets(targets=(), complete=True)complete is False when a bare command substitution ($(...), backticks) was lifted out of the command’s words, so the collection under-counts the real operands; verified is the one-stop safety check — every operand present, statically known, and dequoted.
Parameter Attributes
targets: tuple[Target, …] = ()complete: bool = True
Attributes
| Name | Description |
|---|---|
| verified | Whether the operand list is complete and every target is verified. |
verified
Whether the operand list is complete and every target is verified.
verified: bool
Methods
| Name | Description |
|---|---|
| expand() | Every target’s expansion concatenated: literals contribute themselves, globs their matches. |
expand()
Every target’s expansion concatenated: literals contribute themselves, globs their matches.
Usage
expand(*, limit=GLOB_LIMIT)Reports itself exhausted when any part does or the collection is incomplete.
Expansion
The paths a target’s glob resolves to, plus whether the walk budget was exhausted.
Usage
Expansion(matches, exhausted)matches is capped at limit + 1 entries, so len(expansion) > limit detects an over-cap glob without materializing every match; exhausted is True when a recursive walk blew its 20k-entry budget before completing, or when the targets cannot be statically verified at all — an unverified target, or an operand list a command substitution lifted a word out of. Either way the expansion is incomplete and a guard must not treat it as safe.
Parameter Attributes
matches: tuple[str, …]exhausted: bool
Example
>>> if len(call.targets.expand()) > 10:
... return call.sub("rm", "trash")Command
A single parsed shell command with executable, arguments, env vars, and redirects.
Usage
Command()Use Command.parse(raw) to parse a command string, or access via CommandLine.
Attributes
raw: builtins.str-
The command’s source text.
executable: builtins.str-
The command name, or “” when nothing parsed.
args: tuple[str, …]-
The arguments after the executable.
env: tuple[tuple[str, str], …]-
The leading
VAR=valassignments, as name/value pairs. redirects: tuple[Redirect, …]-
The command’s file redirects.
span: tuple[builtins.int, builtins.int] | None- The command’s byte span in the line, or None when a redirect absorbed a trailing word; excluded from equality and repr.
Attributes
| Name | Description |
|---|---|
| words |
The structural words: words[0] is the executable, words[1:] parallel args.
|
words
The structural words: words[0] is the executable, words[1:] parallel args.
words: tuple[Word, …]
CommandLine
A full parsed bash command line, potentially containing multiple commands joined by operators.
Usage
CommandLine()Use CommandLine.parse(raw) (or the cached parse_command_line) to parse. Access individual commands via .commands or the final command via .primary.
Attributes
raw: builtins.str-
The line’s source text.
parts: tuple[tuple[Command, str | None], …]-
Each command paired with the operator that follows it (None for the last).
commands: tuple[Command, …]-
The parsed commands, in line order.
primary: Command | None-
The final command, or None when nothing parsed.
head: Command | None-
The first command, or None when nothing parsed.
prefixes: tuple[str, …]-
The permission-style prefix of each command, absent prefixes dropped.
q: CommandLineQuery-
The predicate helper for this line.
occurrences: tuple[Occurrence, …]- One Occurrence per part, in line order.
Methods
| Name | Description |
|---|---|
| quote() |
Quote text as one POSIX-sh word, with Python shlex.quote semantics.
|
quote()
Quote text as one POSIX-sh word, with Python shlex.quote semantics.
Usage
quote(text)Redirect
A shell redirect parsed from a bash command (e.g. > file.txt, 2>&1).
Usage
Redirect()Attributes
op: builtins.str-
The redirect operator (
>,>>,2>&1yields>&). target: builtins.str-
The redirect target word.
fd: builtins.int | None- The leading file descriptor number, or None when unspecified.
Word
One structural word of a parsed command (Command.words).
Usage
Word()Attributes: raw: verbatim source text. value: literal with quotes/escapes removed; None when an expansion taints it. span: byte span in the owning CommandLine.raw; None without verbatim source. expandable: bash would glob/brace/tilde-expand despite the literal value.