Prompt Contexts

PromptContext

One declarative XML block attached to an LLM primitive’s prompt.

Usage

Source

PromptContext()

content(evt) runs at evaluation time; None or empty content omits the block. When a required context yields empty content, the primitive skips the LLM call entirely — nothing fires and no fire is consumed.

apply_contexts()

Append each context’s block to prompt in order, each clipped to max_len characters.

Usage

Source

apply_contexts(prompt, evt, contexts, *, max_len=2000)

Over-long content is clipped with an explicit …(+Nch) marker, never a silent truncation. A context yielding None or whitespace-only content has its block omitted; when that context is required, returns None instead — the caller must skip the LLM call entirely.

BeforeEdit

The pending edit’s pre-image, as a <before_edit> block.

Usage

Source

BeforeEdit(tag="before_edit", required=False)

Reads ~captain_hook.events.ToolHookEvent.replaced — an Edit’s old text or a MultiEdit’s joined olds at any event, a Write’s current on-disk content only at PreToolUse (after the Write lands, disk holds the new text). The block is omitted whenever the pre-image is unknowable: non-edit events, Writes off PreToolUse, unreadable paths. Attached to every LLM primitive as a default context; required=False because it is ambient enrichment. Pass BeforeEdit(required=True) explicitly to gate a hook on a non-empty pre-image.

Parameter Attributes

tag: str = "before_edit"
required: bool = False

AfterEdit

The pending edit’s new text, as an <after_edit> block.

Usage

Source

AfterEdit(tag="after_edit", required=False)

Reads ~captain_hook.events.BaseHookEvent.content — the text an Edit, Write, MultiEdit, or NotebookEdit is about to land. Attached to every LLM primitive as a default context; required=False because it is ambient enrichment, empty (and omitted) on non-edit events.

Parameter Attributes

tag: str = "after_edit"
required: bool = False

Introduced

Constructs the pending edit newly introduces, as an auto-tagged block.

Usage

Source

Introduced(kind=None, pattern=None, required=True, tag=None)

Exactly one of kind/pattern selects the constructs: kind names tree-sitter node kinds (a bare string or any set — normalized to frozenset; see ~captain_hook.ast_grep.COMMENT_TYPES), pattern is an ast-grep pattern. Extraction diffs the event’s before/after text, so only constructs absent before the edit appear; files without a supported language yield nothing. The pre-image comes from evt.replaced, so hooks covering Writes need events=Event.PreToolUse — at any other event a Write’s pre-image is unknowable, the context yields None, and (being required) the LLM call skips rather than misreporting every construct as introduced.

required defaults to True: a context you attach explicitly IS the evidence — no evidence, no LLM call. tag auto-derives from the class name in snake_case (Introduced() renders <introduced>; a subclass named TombstoneComments renders <tombstone_comments>). Subclass and override keep() to filter which introduced constructs count.

Parameter Attributes

kind: str | Set[str] | None = None
pattern: str | None = None
required: bool = True
tag: str | None = None

Example

>>> llm_nudge("...", contexts=[Introduced(pattern="print($$$)")],
...           events=Event.PreToolUse, only_if=[Tool("Edit", "Write", "MultiEdit")])

Methods

Name Description
keep() Whether an introduced construct’s text belongs in the block — override to filter.
keep()

Whether an introduced construct’s text belongs in the block — override to filter.

Usage

Source

keep(text)

WorkflowScriptSource

Gating context: the pending Workflow call’s script source, headed by its model pins.

Usage

Source

WorkflowScriptSource(tag="workflow_script", required=True)

Resolves the script via ~captain_hook.conditions.workflow_script_source(), excerpts the text around every model: pin into a header capped at PIN_EXCERPT_CAP, and truncates the body past WORKFLOW_SCRIPT_CAP with an explicit marker. Yields None off a Workflow event or an unreadable script.

Parameter Attributes

tag: str = "workflow_script"
required: bool = True

UserMessages

The session’s user prompts as a <user_messages> request/authorization record.

Usage

Source

UserMessages(last=4, per_message=600, tag="user_messages", required=True)

Collects every real user prompt from the transcript — turn-opening text under the native classifier, so meta and hook-injected user events are excluded — then renders the first prompt followed by the most recent last, deduped where they overlap. Each prompt is clipped to per_message characters with an explicit …(+Nch) marker and prefixed [first] or [recent -N] (-1 most recent) so a judge can order them. The first prompt leads so the original ask survives the tail clip apply_contexts() applies. Yields None when the session carries no user prompt; being required by default, that skips the LLM call rather than judging a hook against an empty authorization record.

Attributes

last: int

How many of the most recent prompts to render after the first.

per_message: int

Character budget each rendered prompt is clipped to.

tag: str

The XML block tag; defaults to user_messages.

required: bool
Whether empty content skips the LLM call; defaults to True.

Example

>>> llm_gate("does this edit stay within what the user asked?", contexts=[UserMessages()])

Excerpts

Verbatim excerpts pulled from a text under a character budget.

Usage

Source

Excerpts(excerpts, quoted, dropped)

excerpts are the kept windows in source order; quoted counts the spans they cover and dropped the spans excluded once the budget filled. Render with block().

Attributes

excerpts: tuple[str, …]

The kept excerpt strings, in source order, without indentation.

quoted: int

How many input spans the kept excerpts cover.

dropped: int
How many input spans the budget excluded.

Attributes

Name Description
capped Whether the budget excluded at least one span.
capped

Whether the budget excluded at least one span.

capped: bool

Methods

Name Description
block() Render the excerpts as an indent-prefixed block.
block()

Render the excerpts as an indent-prefixed block.

Usage

Source

block(noun, *, indent="  ", empty="(none)")

A capped block ends in a … [+N more <noun> not excerpted] marker line; an empty one renders the empty placeholder.

Parameters
noun: str

Plural noun for the dropped-count marker, e.g. "model pins".

indent: str = " "

Prefix applied to every line, the marker and placeholder included.

empty: str = "(none)"
Placeholder rendered when there are no excerpts.

excerpt_around()

Verbatim excerpts of text around each (start, end) character span.

Usage

Source

excerpt_around(
    text, spans, *, before=160, after=60, whole_line_at=200, budget=2000
)

Spans are whole-text character offsets — re.Match.span() is the natural producer, and ast-grep callers can pass node.range().start.index / node.range().end.index. Spans group by line: a line at or under whole_line_at characters is quoted whole in one window; a longer line yields one window per span, clamped to the line as [start - before, end + after] and merged when windows overlap or touch, each bracketed by only where text was actually elided. Excerpts accumulate in source order under a greedy character budget — the first excerpt is always kept, and once the budget fills every later span is dropped.

Parameters

text: str

The source text to excerpt from.

spans: Sequence[tuple[int, int]]

(start, end) character spans into text, in source order.

before: int = 160

Characters of context kept before each span on a long line.

after: int = 60

Characters of context kept after each span on a long line.

whole_line_at: int = 200

Lines at or under this length are quoted whole.

budget: int = 2000
Character budget across the kept excerpts.

COMMENT_TYPES

Tree-sitter node kinds that denote a comment, across every supported grammar.

COMMENT_TYPES: frozenset[str] = GENERATED_COMMENT_TYPES

The union covers every [LANG_GLOBS][captain_hook.langs.LANG_GLOBS] grammar. Markdown (md) is the verified exception with no comment kinds. A Dart block documentation comment has an outer comment with a nested documentation_block_comment; its grammar has no literal documentation_comment kind. Derived at build time from each grammar’s node-types.json: the named kinds whose name contains comment, minus any referenced as a child inside another comment kind (Rust’s doc_comment and its markers, Dart’s nested block). Generation fails loud if any grammar but Markdown yields no comment kinds.

Edit

The pending edit’s before/after source, parsed for structural queries.

Usage

Source

Edit(old_text, new_text, lang)

Reached via [evt.edit][captain_hook.ToolHookEvent.edit]; None there when the event carries no edit or the file’s language has no grammar. old and new are lazily parsed [SyntaxNode][captain_hook.SyntaxNode] trees; matches and introduced take ast-grep pattern strings ("print($$$)").

Parameter Attributes

old_text: str
new_text: str
lang: str

Example

>>> if evt.edit and (added := evt.edit.introduced("print($$$)")):
...     return evt.warn(f"New print() calls: {[m.line for m in added]}")

Methods

Name Description
introduced() Matches of pattern present after the edit but absent before it, by whitespace-normalized text.
matches() Whether pattern matches anywhere in the post-edit source.
introduced()

Matches of pattern present after the edit but absent before it, by whitespace-normalized text.

Usage

Source

introduced(pattern)
matches()

Whether pattern matches anywhere in the post-edit source.

Usage

Source

matches(pattern)

SyntaxNode

One node of a parsed syntax tree — the framework’s face over the ast-grep binding node.

Usage

Source

SyntaxNode(raw)

Parameter Attributes

raw: SgNode

Methods

Name Description
descendants() Every node below this one, in document order.
descendants()

Every node below this one, in document order.

Usage

Source

descendants()

Match

A structural match, located by 1-based line to align with Violation and changed-line scoping.

Usage

Source

Match(line, end_line, text)

Parameter Attributes

line: int
end_line: int
text: str