Documentation
¶
Overview ¶
Package hookrules implements regex-based rules for the `agnt hook` subcommand family that redirects raw Bash usage and user prompts toward agnt MCP tools.
The CLI hot path is:
check-bash: read PreToolUse JSON → find Bash tool → match command against BashRules → emit decision (allow/warn/block) within a 1s budget. check-prompt: read UserPromptSubmit JSON → match prompt against PromptRules → emit a <system-reminder> on stdout if matched.
Rules are compiled once (via sync.OnceValue) and re-used for the life of the process. A rule set is the union of built-in defaults and any overrides loaded from .agnt.kdl's `hook-rules` block.
Design rule: on any error (regex compile failure, config parse failure, daemon down for scope guard) the package returns a "no-op allow" decision. Breaking the agent's tool call is worse than failing to intercept.
Index ¶
Constants ¶
const AgntRunEnv = "AGNT_RUN"
AgntRunEnv is the environment variable `agnt run` stamps into the wrapped child process so downstream tooling can detect "I am inside an agnt run session". The hook interceptor uses it to decide whether a hard block (exit 2) is appropriate: inside a session the wrapped agent has proc/proxy wired and gets the stdin-injected guidance, so a block cleanly redirects it. Outside a session (e.g. a plain Claude Code session that merely happens to sit in a project with a `.agnt.kdl`) a block would surface as a spurious error, so blocks are downgraded to a non-error soft-warn instead.
const BypassMarker = "# agnt-allow"
BypassMarker is the inline comment marker that, when present anywhere in a Bash command, causes the interceptor to fail-open for that single invocation. "# agnt-allow" is unambiguous (unlikely to collide with real shell usage) and visible in the command string the daemon logs.
const DefaultBypassEnv = "AGNT_HOOK_BYPASS"
DefaultBypassEnv is the env var whose presence short-circuits the hook interceptor. Exported so the CLI layer can reference it in help text.
Variables ¶
This section is empty.
Functions ¶
func CommandHasBypassMarker ¶
CommandHasBypassMarker returns true if the command string contains the inline bypass marker. The check is a plain Contains — we do NOT try to parse shell comments because the marker is opt-in and the exit contract must be fast.
func EnsureRulesDocPath ¶
EnsureRulesDocPath returns the absolute path to the project-local docs/hook-rules.md file if present, empty string otherwise. Used by the `rules list` subcommand to include a doc pointer in its output.
func InAgntRunSession ¶ added in v0.13.23
InAgntRunSession reports whether the current process is running inside an `agnt run` PTY session, detected via the AgntRunEnv flag. getenv is injected so callers (and tests) control the lookup.
func ScopeGuardActive ¶
ScopeGuardActive reports whether hook interception should apply for the given project directory. The intent is: avoid false positives in random unrelated directories. Interception applies when ANY of these is true:
- `.agnt.kdl` exists at or above projectDir
- projectDir is non-empty (weaker fallback — the caller passed a real project path, so we trust it)
The "daemon has a process/proxy for this path" check described in the task is intentionally omitted here: the scope-guard runs on the hot path and must not block on an IPC round-trip to a potentially wedged daemon. The 50ms daemon-connect deadline called out in the task is a stricter requirement than what we'd get from a best-effort guard, so we punt on it entirely. The .agnt.kdl presence check is cheap and catches the common case (user opened a non-agnt repo in Claude Code).
When projectDir is empty (Claude Code did not pass --project-path), we return true — fail-closed on scope would silently disable interception for the majority of real invocations where the hook wiring simply forgot the flag.
Types ¶
type Action ¶
type Action string
Action is the decision emitted by matching a Bash rule.
const ( // ActionAllow means the command was not matched or is explicitly allowed. ActionAllow Action = "allow" // ActionSoftWarn emits a stderr nudge but still exits 0, so the tool call // proceeds. Used for patterns that are often fine but sometimes better // served by an MCP tool. ActionSoftWarn Action = "soft-warn" // ActionBlock emits exit code 2 with a stderr redirect, preventing the // tool call from executing. Used for patterns that have a clean MCP // equivalent (dev-server start, kill, proxy log tail, etc.). ActionBlock Action = "block" )
type BashRule ¶
type BashRule struct {
// Pattern is the compiled regex matched against the Bash command string.
Pattern *regexp.Regexp
// Raw is the source pattern string — preserved so `rules list` and
// `rules test` output cite the human-readable form, not the compiled
// regex syntax.
Raw string
// Action is the decision emitted on a match.
Action Action
// Replacement is a short citation of the recommended MCP invocation.
// Empty means "no replacement; see docs."
Replacement string
// Reason is optional prose appended to the stderr message. Use to
// explain *why* the command was redirected when the replacement alone
// is not self-explanatory.
Reason string
}
BashRule is a compiled pattern against the Bash command string with an associated action and optional replacement text cited in the stderr message. Replacement is free-form — typically an agnt MCP tool name with a hint of parameters — so operators can see exactly what to call instead.
type Decision ¶
type Decision struct {
Action Action
Rule *BashRule // nil when Action == ActionAllow
Replacement string
Reason string
}
Decision is the output of MatchBash. The zero value is a valid "allow" decision so callers can treat a nil-error, empty-pattern input as a no-op.
type PromptRule ¶
type PromptRule struct {
// Pattern is the compiled regex matched against the prompt text. All
// patterns are case-insensitive by default because prompt wording is
// colloquial, not a command line.
Pattern *regexp.Regexp
// Raw is the source pattern string for `rules list`.
Raw string
// Reminder is the body of the <system-reminder> emitted on match.
Reminder string
}
PromptRule is a compiled pattern against the user's prompt text with an associated system-reminder body injected when it matches. Prompt rules never block — they only nudge.
type RuleSet ¶
type RuleSet struct {
BashRules []BashRule
PromptRules []PromptRule
// BypassEnv is the env var whose presence (set to any non-empty value)
// causes check-bash / check-prompt to fail-open. Defaults to
// "AGNT_HOOK_BYPASS"; configurable via .agnt.kdl.
BypassEnv string
}
RuleSet is the merged collection of Bash and prompt rules used by the check-bash / check-prompt subcommands. Rules are evaluated in order; first match wins for Bash (so higher-priority patterns should be listed first in both builtin defaults and the KDL override).
func BuiltinRuleSet ¶
func BuiltinRuleSet() *RuleSet
BuiltinRuleSet returns the default rule set compiled from the hardcoded patterns below. Compilation happens once on first call; panics in compile are caught as static tests (see rules_test.go TestBuiltinRuleSet).
The current catalog is intentionally small (nine Bash patterns, two prompt patterns) so the regression corpus stays manageable and the rules remain easy for a human to scan. New patterns should land as KDL-side overrides first and only get promoted to builtins after they have enough production signal.
func LoadAndValidate ¶
LoadAndValidate is the non-silent loader for the `rules` subcommand. It returns the merged rule set plus any per-pattern errors so the CLI can surface them to the operator. Unlike LoadForProject, it does not swallow regex compile failures — they come back in the errors slice.
func LoadForProject ¶
LoadForProject returns the merged rule set for the given project path. Built-in rules are always present; KDL overrides (from `.agnt.kdl` `hook-rules` block) are appended so a user-provided rule evaluates AFTER builtins. A user who needs to override a builtin pattern should do it by negating via a more specific pattern earlier in evaluation order — which today means editing the builtin catalog, since override-before-builtin semantics would require per-project reordering we have deliberately not built.
Errors loading config fall through silently to builtin-only. The callers (check-bash / check-prompt) have a 1s fail-open budget and must not surface config errors as hook failures.
func MergeOverrides ¶
func MergeOverrides(base *RuleSet, override *config.HookRulesConfig) *RuleSet
MergeOverrides returns a new RuleSet that combines the builtin base with the parsed KDL override block. It does not mutate base. Invalid patterns in the override are silently skipped — consistent with the fail-open contract — but LoadAndValidate (below) surfaces them for `rules` dry-run.
func (*RuleSet) MatchBash ¶
MatchBash runs the command string against each rule in order and returns the first match's decision. An empty command is always Allow (no rule fires) — callers typically get here from a malformed PreToolUse payload and the fail-open contract takes over.
func (*RuleSet) MatchPrompt ¶
func (rs *RuleSet) MatchPrompt(prompt string) []PromptRule
MatchPrompt returns the list of prompt rule reminders that match the input text. Unlike MatchBash, prompt matching is additive — multiple reminders can fire on a single prompt (e.g., a prompt mentioning both "start the server" and "check errors" should pick up both nudges).