Skip to content

Latest commit

 

History

History
92 lines (85 loc) · 22.4 KB

configuration.md

File metadata and controls

92 lines (85 loc) · 22.4 KB

hookprobe — configuration

Every variable, its default, and why it exists. The terse generated version — names and defaults only, straight from hookprobe/settings.py — is reference.md; that one cannot drift from the code, and this one carries the reasoning that a generator has nowhere to put.

env default meaning
HOOKPROBE_TOKEN (empty = unauthenticated) Bearer token callers must present
HOOKPROBE_MODEL claude-opus-5 Model for the agent session
HOOKPROBE_MAX_TURNS 32 Hard agent-loop budget per run
HOOKPROBE_MAX_CONCURRENT 2 Parallel runs; the rest queue
HOOKPROBE_DEFAULT_TIMEOUT_SECONDS 900 When the trigger omits timeoutSeconds
HOOKPROBE_MAX_TIMEOUT_SECONDS 1800 Upper clamp on requested timeouts
HOOKPROBE_WORKDIR /data Persistent workspace (skills, results)
HOOKPROBE_MCP_CONFIG (unset) Path to an .mcp.json-shaped file of MCP servers
HOOKPROBE_MCP_TOOLS (empty = none may run) The closed set of MCP tools this instance may call: mcp__chat__chat_search_messages, or mcp__webhookwise__* for a whole server. Mounting a server does not grant its tools. There is no such thing as a read-only MCP server — a chat server ships send_message beside search_chat_records — and this component reads attacker-influenced text, so "let it read the thread" and "let a message in that thread post as you" must not be the same decision. Enforced by a PreToolUse hook, like the bash guard, because permission_mode is bypassPermissions and an allowlist there is a preference rather than a gate. A refusal names what the instance may call, so the wrong list is a readable error and not a mystery
HOOKPROBE_EVENT_SECRET (empty = unsigned) Verifies the pipe's deliveries to /hooks/event. This is the one mutating route HOOKPROBE_TOKEN does not cover — the pipe delivers there, so the signature is its credential — and it is the only door that starts paid investigations. Setting the token and leaving this empty locks every door a person uses and leaves that one open; boot says so in the log
HOOKPROBE_ACTION_SECRET (empty = the event secret) The card door's own credential (/hooks/action). Falls back to HOOKPROBE_EVENT_SECRET so an existing deployment keeps one secret; set it when the watcher/planner (which read colleagues' messages) must not hold the key that can rule a run or press a card. With BOTH empty the door is unsigned, and approve refuses — it is the one kind that executes, and a run inside the container can read the proposal and its hash
HOOKPROBE_RETURN_URL (unset = no return) Where event-door investigations report back — the pipe's probe-notify front door
HOOKPROBE_RELAY_UI_URL (unset = no link) Where the pipe's board is as a browser reaches it (a tunnelled http://127.0.0.1:8100, not the container name); a run's page then links to the chain it belongs to
HOOKPROBE_RETURN_SECRET (empty = unsigned) Signs the return delivery (timestamped HMAC)
HOOKPROBE_RULING_URL · HOOKPROBE_RULING_SECRET (both unset = off) Where a retrospective condition ruling goes (the judge's /rulings/ai) and the credential for that one door. A verdict is worth_it or not_worth_it and must carry a reason; anything else is dropped before it is sent. The agent PROPOSES with an AI-RULING: line and the service signs — the same division as MEMORY-SUGGESTION, because the agent is the component that reads attacker-influenced text and does not get a reusable key for a sibling's ledger
HOOKPROBE_ESCALATE_LEVELS critical,high The only content judgement the investigator makes: which levels are worth a paid run
HOOKPROBE_DECLINE_PATTERNS (unset = decline nothing) Path to a file of full-match regexes over alert titles — the families this node has no instrument for — declined at the event door with status: skipped and the pattern that matched, before any run is opened. One per line, # comments, hot-read on every event; a line that is not a regex is logged and ignored rather than declining everything. Rule-driven escalations only: a run a person opened from chat (fields.sender) is never declined, and /hooks/agent is untouched. Mirrors the upstream platform's skip_deep_analysis inbound rules, which excluded the mail, queue, cache and managed-database families on 2026-09-01 because the investigator cannot reach their data — and the pipe's leg then funded twenty investigations of one of them in a week, 70% of the investigator's spend. The example is hookprobe/deploy/operator/decline-patterns.example; the production compose mounts that directory read-only at /etc/hookprobe/operator/. Every decline is a line in declines.jsonl under the workdir, and GET /v1/declines?hours= counts them — events, distinct conditions, per pattern, with configured saying whether there is a list at all — for the weekly page
HOOKPROBE_SYNTHETIC_KEY_PREFIXES manual:,drill: Session-key prefixes that mark a run synthetic: a drill, a by-hand check, a wiring test. Real machinery on unreal work, so it is kept out of the books — no runbook is distilled from it, it anchors no re-fire answer and vouches for nothing, and the weekly page and the work board leave it out. Three of the twenty runbooks on the production shelf were distilled from such runs before this existed
HOOKPROBE_VERDICTS (empty = off) A comma-separated closed vocabulary this instance may CONCLUDE with. Declared, the prompt asks the report to end with VERDICT: <one of them> and the return delivery carries it as meta.verdict, which a pipe extracts with fields: {verdict: "{meta.verdict}"} and routes on. It is the one field on the return trip the investigator decides — meta.importance is the level of the event that came in — so it is what lets a report be the MIDDLE of a chain instead of a leaf. Closed rather than free text because a routing key decides where money is spent, in the component that reads attacker-influenced prose: an undeclared label yields "", never a guess. See the decision note
HOOKPROBE_BUDGET_USD 0 (off) Window spend ceiling for the event door; refusals report themselves. GET /v1/budget shows the arithmetic
HOOKPROBE_BUDGET_WINDOW_HOURS 24 The sliding window the budget is measured over
HOOKPROBE_RETENTION_DAYS 0 (keep all) Case files and transcripts older than this are pruned daily; skills and memory are never touched
HOOKPROBE_AUTO_DISTILL_MAX 0 (manual) How many runbooks finished runs may leave behind. Above 0, each completed run writes its own .claude/skills/<name>/SKILL.md — from the service, create-only, marked unreviewed. See The learning loop
HOOKPROBE_MEMORY_AUTO_APPLY 1 (on) The prompt invites at most one MEMORY-SUGGESTION: line per report and the service lifts it out. A line whose SHAPE cannot act — no imperative, no second person, no URL, no shell metacharacter, one line, under 400 characters — is appended to CLAUDE.md under its own unverified heading. Anything else is queued for a person, which is what the queue is now for. Set 0 to queue everything. The agent's own tools reach neither the queue nor the memory either way
HOOKPROBE_REMEDIATION_ALLOWLIST (unset = collect-only) Path to a file of full-match regexes, one per line, hot-read at execution time. A report may append a fenced remediation block; the service parks it as a proposal on the actions page, and approving runs the steps sequentially, stop-on-failure, each audited. Deny-by-default: no allowlist, no execution. The read-only investigator never runs these — the service does, after an operator's click. The example is hookprobe/deploy/operator/remediation-allowlist.example; the work compose mounts that directory read-only at /etc/hookprobe/operator/ on the write node. A step the report itself marks high must ALSO full-match HOOKPROBE_REMEDIATION_HIGH_RISK_ALLOWLIST, so arming the executor does not arm its worst half by the same gesture
HOOKPROBE_BLAST_RADIUS (unset = recorded, not judged) For a writing node (HOOKPROBE_BASH_GUARD=danger-only) only: a file naming what its credential is allowed to do, one line per entry, copied verbatim from what GET /v1/posture reports under kube.mutating and aws.allowed. # comments, blanks ignored. Under HOOKPROBE_POSTURE_CHECK=enforce a credential that can do anything not on the list refuses to start and names the excess, and the hourly selftest re-measures against the same pin, so a credential that grows while the node runs fails within the hour instead of waiting for a restart. The workflow is pin what you saw: boot once, read the measurement, paste the lines you meant to grant. Unset leaves the behaviour a writing node has always had — the check records the blast radius and judges none of it. A path that cannot be read declares an EMPTY radius, not an absent one, so a typo fails closed
HOOKPROBE_REMEDIATION_HIGH_RISK_ALLOWLIST (unset = no high-risk step runs) The second gate, same format and same hot read as the first. A step the report itself marked risk: high must full-match a pattern in both files; a low or medium step is unaffected by this one. Deny-by-default in the same direction as the allowlist, so arming remediation does not arm its worst half by the same gesture — configure the ordinary list alone and every high-risk step is refused with a reason naming this variable. The label is the model's, so this can only tighten: a dangerous command the report called low is bounded by the ordinary allowlist exactly as it was before this existed. Reserve a stricter list for the commands the model is willing to call dangerous; do not read it as "dangerous commands need two patterns"
HOOKPROBE_REMEDIATION_VERIFY_SECONDS 3600 (0 = off) How long an executed procedure waits for the condition to answer before the board calls it held. Exit 0 is where the commands' story ends and the condition's begins: a recovery arriving at any hour after the run holds the procedure and verifies the work (verified_by: remediation); a re-fire of the same alert inside this window says it did not hold, and the row, the console and the audit (tool: Outcome) say so while the re-fire itself becomes a follow-up turn; the window closing with nothing said holds it in the weakest words the vocabulary has — "no re-fire within the window" — because the absence of a re-fire is thinner evidence than a target re-read. The first answer stands: a recovery after a re-fire does not launder it. Until this landed (2026-09-28) the board counted every clean run as verified, so a fix that fixed nothing and a fix that worked read identically
HOOKPROBE_REMEDIATION_COOLDOWN_SECONDS 900 How long one target is left alone after a procedure has acted on it. A second proposal that shares a step's declared target or its literal command is refused for this long — both, because a model labels one thing several ways across runs while the command it writes stays identical, and one already executing holds its target regardless of the window. The refused row stays proposed — held, not retired — and the same approval works once the window passes. 0 disables it. Defaults ON, unlike the allowlist: it costs nothing where remediation is unused, and a fix approved twice in a minute is reachable the moment remediation is armed at all
HOOKPROBE_CONSOLIDATE_AT 5 (0 = off) At this many accumulated cases, a runbook triggers ONE agent run that distills the pile into a curated procedure. The draft is APPLIED, snapshotting what it displaced — auto_write already installs these manifests with no human, so a gate on consolidating them guarded a class of text already arriving unguarded, and the draft simply stalled. Reversibility replaces permission: POST /v1/skills/{name}/history/{stamp}/restore puts the old version back in one call, and reviewed stays false because a machine writing a file does not make it read
HOOKPROBE_RULING_TTL_DAYS 14 (0 = off) How long a standing not_worth_it ruling (filed by the weekly patrol, kept in rulings.jsonl beside the case files) may answer that condition's re-fires from its runbook instead of starting a paid run. The reply is report-shaped JSON marked answered_from_runbook, the run costs $0, and {"force": true} on the trigger bypasses it. A person files a standing verdict through POST /v1/rulings (console bearer); while current it outranks the patrol's inference for that condition, since latest-wins alone would let Thursday's refile overwrite a decision
HOOKPROBE_RUNBOOK_ANSWER_DAYS 0 (off) The cost mirror of the ruling TTL above. A condition whose most recent REAL investigation a person ruled useful answers a re-fire from that runbook for this many days instead of paying for a cold-start — the daily-recurrence gap the coalesce window (minutes) cannot reach. It never silences: the re-fire still delivers a card, marked $0 and answered-from-runbook, carrying the procedure and how to force a real run if it looks different. The gate is a USEFUL press, so a useless one (which withdraws the runbook) stops it, and only a real run vouches — a runbook answer never vouches for the next, or the chain would cite itself. Once the window lapses a real run reverifies. Off by default: answering a worth-it condition from a runbook is a stronger claim than declining a not-worth-it one
HOOKPROBE_REFIRE_ANSWER_HOURS 0 (off) The third $0 path, and the one that waits for no ruling. A re-fire of a condition inside this many hours of its last REAL investigation — same source and title, same level, no recovery recorded since, and that investigation completed cleanly — is answered from that investigation's report (plus the runbook when one exists) instead of buying a cold start. Measured before it existed (2026-09-14): one SES condition re-fired every four hours through a day, six cold starts reached one finding, and the ruling that would have licensed the row above arrived two days later; the coalesce window (minutes) never sees a four-hour cadence. Bounded at ten answers per real run so a flapping alert still buys a real look; a runbook answer never anchors the next; a newer real run that failed forfeits the anchor; {"force": true} bypasses it. Never a silence: the re-fire still delivers a card, marked $0 and answered_from_runbook, naming the run it answered from in refire_of
HOOKPROBE_RULING_REVERIFY_DAYS 7 A ruled-useless condition still gets a REAL investigation this often. Gated answers do not count as verification — only a run that actually looked does, or the gate would feed itself forever
HOOKPROBE_COALESCE_WINDOW_SECONDS 1800 A re-fire of the same alert (same source+title, new event id) inside this window becomes a follow-up turn in the existing session instead of a new cold-start investigation. Redelivery of the same event id stays idempotent. 0 disables
HOOKPROBE_REPEAT_REMINDER_AT 3 (0 = off) After N identical tool calls, remind the agent to change approach
HOOKPROBE_HANDOFF_URL · HOOKPROBE_HANDOFF_SECRET (both unset = off) Where the sessions page's Hand off button posts a finished report, and the credential for that one door. A PIPE door, never another node's: the pipe is what makes a handover accountable, and going through the front door means the chain the ledger already joins simply grows a hop. Dedup is left to it — two clicks produce one fingerprint, so the second is recorded as a duplicate rather than buying a second run. The button is only offered when the URL is set; an offer that answers 501 is worse than no offer. Since 2026-09-28 the same press is on the plan's CARD (handoff in the pipe's card_actions), so the approval no longer needs the console — the card's press and the page's click post the same bytes to the same door and leave the same record (handed_off_at, handed_off_by, one audit line)
HOOKPROBE_AUTOMATION_TIERS (empty = defaults) The declared ceiling per class of automation, class=tier comma-separated over the defaults — memory=propose,remediation=propose. The ladder is verdict_only < investigate < propose < auto_apply; a class may do at most its tier, and the scattered auto-apply knobs (HOOKPROBE_MEMORY_AUTO_APPLY, the remediation allowlist, HOOKPROBE_ESCALATE_LEVELS) operate UNDER it — a knob cannot grant what the tier forbids. Defaults preserve today's behaviour, so this is inert until a line is written. It is not what makes graduation safe — the RECORD is: every class accumulates proposals and the human decisions on them (GET /v1/automation), and raising a ceiling is justified by that record, never by an argument or an agreement rate. A typo in a tier name keeps the default rather than raising it. See automation.py and the sampling patrol
HOOKPROBE_BASH_GUARD readonly Which posture the bash guard takes. readonly refuses the mutating verbs of the CLIs an SRE agent reaches for — the investigator's posture, and the only one that should face an event door. danger-only allows them and refuses a much shorter list instead: the handful whose damage is to the container, the host or a whole estate rather than to the object an operator scoped a credential to. It is for a runner an operator gave scoped WRITE credentials to, and it is not what bounds such a runner — the credentials are. An allowlist would be the wrong shape here and guard.py says why. An unknown value falls back to readonly: a typo in the variable that decides whether a runner may write must fail closed
HOOKPROBE_POSTURE_CHECK enforce Whether the credentials are measured against that declaration at startup (posture.py): kubectl auth can-i --list plus four cluster-wide mutating questions, aws sts get-caller-identity plus iam simulate-principal-policy over a short list of dangerous actions. enforce refuses to start a runner declared readonly whose credentials can mutate; warn starts and says so; off skips. A danger-only runner that has declared no blast radius is recorded rather than judged — there is nothing to compare against; once HOOKPROBE_BLAST_RADIUS names a file, enforce means the same sentence on both postures, and a credential wider than the declaration refuses to start. Unverifiable (a read-only identity that may not simulate itself) is its own verdict, never confirmed. Result in posture.json, on GET /v1/posture, and inside every run's audit as startup_posture. A typo falls back to enforce
HOOKPROBE_TELEMETRY_RECEIVER on Whether this service receives its own runs' OpenTelemetry. on points the bundled CLI at POST /otel/v1/{logs,metrics} on this service, keeps a per-run timing record under {workdir}/telemetry/ (numbers and names only — no prompt, tool or response text, whatever the OTEL_LOG_* switches say), draws it as the waterfall on the sessions page, and forwards every body untouched to OTEL_EXPORTER_OTLP_ENDPOINT when the service has one. off leaves the CLI's exporter alone: telemetry goes wherever the container's OTEL_* variables say, or nowhere, as before.
HOOKPROBE_FOLLOW_UP_SENDERS (empty = nobody) Who may spend a turn from chat — continue an investigation from its thread, or open a topic that starts one (fields.sender present on the event): the pipe forwards a reply under one of its cards as an event with fields.kind: follow_up, and this door continues the investigation's own engine session with the question — the same read-only posture, the same budget breaker, one turn per message id, at most 20 per run — and answers back into the thread. Comma-separated platform sender ids, or * for anyone the bridge forwards from the group. Empty refuses every follow-up, because a reply in a group is a paid turn anyone in the group could start.
HOOKPROBE_BASH_TIMEOUT_MS 120000 (0 = CLI default) Deadline for a single command (BASH_DEFAULT_TIMEOUT_MS)
HOOKPROBE_BASH_MAX_TIMEOUT_MS 600000 (0 = CLI default) Ceiling the agent may request per command (BASH_MAX_TIMEOUT_MS)
CLAUDE_CODE_ENABLE_TELEMETRY + OTEL_* (unset = off) Passed to the CLI, which emits one OpenTelemetry event per model call. No default endpoint — see cost.md
HOOKPROBE_HOST / HOOKPROBE_PORT 0.0.0.0 / 8088 Bind address
ANTHROPIC_API_KEY — Or ANTHROPIC_BASE_URL + ANTHROPIC_AUTH_TOKEN for a relay

Managing MCP servers by hand

MCP is managed as one JSON file — no API writes, on purpose: server specs carry credentials in their env, and secrets belong in a file you mount, not in a web form. The loop is:

  1. Write the file. Three dialects are accepted: the bare {name: {command, args, env}} mapping, the .mcp.json wrapper ({"mcpServers": {...}}), and the marketplace config.json shape — entries with "enabled": false are skipped and the flag is stripped, so downloaded MCP packages work unedited.
  2. Mount it read-only, outside the run's volume, and point HOOKPROBE_MCP_CONFIG at it (see the commented lines in every compose). It stays editable from the host, and the next run reads the edit. Not on the volume: the engine starts the stdio servers the file names at the top of every run, so a copy the agent can write is a command one run can plant for every run after it. The input guard refuses the obvious writes to it wherever it lives, and the startup posture lists it under writable_inputs while this node's own user could still write it.
  3. Verify with GET /v1/mcp: it reads the file fresh and shows each server's command/args/type/url plus its env key names only — env values never leave the file.
  4. Edit any time: the config is read fresh at every run (and every /v1/mcp call), so changes apply to the next investigation without a restart.

Browser evidence (optional)

Give the agent an interactive browser for dashboards that have no API: copy deploy/mcp.example.json (a headless Playwright MCP server), point HOOKPROBE_MCP_CONFIG at it, and uncomment the chromium block in the Dockerfile so the image ships the browser. One caution: a browser can click and submit on any page it can reach — the bash guard does not see browser actions, so point it at read-only dashboards and viewer accounts; --isolated keeps it from retaining any profile state between runs.