hookrules

package
v0.13.34 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 10, 2026 License: Apache-2.0 Imports: 8 Imported by: 0

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

View Source
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.

View Source
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.

View Source
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

func CommandHasBypassMarker(cmd string) bool

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

func EnsureRulesDocPath(projectDir string) string

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

func InAgntRunSession(getenv func(string) string) bool

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

func ScopeGuardActive(projectDir string) bool

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

func LoadAndValidate(projectDir string) (*RuleSet, []error)

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

func LoadForProject(projectDir string) *RuleSet

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

func (rs *RuleSet) MatchBash(command string) Decision

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).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL