Webhooks & Event Hooks¶
CodeNavi emits outbound, metadata-only events for its main lifecycle moments — indexing, crawling, secret detection, chat, and licensing. Use them to drive local automation (freshness pipelines, security/audit feeds, ops alerts) without anything leaving the box.
Two independent delivery channels, both off by default, both safe to run in airgapped / privacy-sensitive deployments:
| Channel | Setting | What happens |
|---|---|---|
| HTTP webhook | HOOK_URL |
CodeNavi POSTs the event as JSON to the URL |
| Local script | HOOK_SCRIPT_DIR |
CodeNavi runs <dir>/<event>.sh with the JSON on stdin |
Both can be active at once. Delivery is fire-and-forget on a background thread — a hook can never slow down or break indexing/chat; failures are logged and dropped (no retries, no queue).
Why this is airgap-friendly¶
- The script channel needs zero network.
reindex.completed.shruns locally on the box — no socket, no DNS. - The HTTP channel can target local infra — a self-hosted Mattermost/Rocket.Chat,
Prometheus Alertmanager, a SIEM (Wazuh/Elastic/Splunk), n8n/Node-RED. Point
HOOK_URLathttp://something.internal/…and the traffic stays on your LAN. - Payloads are metadata-only — never code, never secret values. A
scanner.blockedevent carries the file path and the rule IDs that matched, never the matched secret. So events are safe to forward even in environments that are paranoid about everything else.
Enabling¶
Set via environment variables, data/settings.json, or the web UI (System → Hooks):
| Setting | Default | Meaning |
|---|---|---|
HOOK_URL |
(empty) | HTTP endpoint to POST events to. Empty = HTTP channel off. |
HOOK_SCRIPT_DIR |
(empty) | Directory of <event>.sh scripts. Empty = script channel off. |
HOOK_TIMEOUT_SEC |
5 |
Per-POST timeout for HOOK_URL. |
HOOK_SCRIPT_TIMEOUT_SEC |
30 |
Per-script timeout before it's killed (independent of HOOK_TIMEOUT_SEC — scripts typically need longer than a webhook POST). |
HOOK_URL / HOOK_SCRIPT_DIR apply live when saved via the UI/settings; HOOK_TIMEOUT_SEC
is read per call.
The payload envelope¶
Every event — webhook body or script stdin — has the same JSON shape:
{
"event": "reindex.completed",
"data": { "...": "event-specific fields" },
"ts": 1750650000.123
}
event— one of the names below.data— event-specific fields (table).ts— Unix timestamp (float seconds).
Event reference¶
| Event | Fires when | data fields |
|---|---|---|
reindex.started |
a reindex sweep begins | full (bool), codebases (list) |
reindex.completed |
a reindex sweep ends (encoding done; a VRAM post-step may follow) | full, codebases, status (ok|cancelled|failed), encoded (int — chunks re-embedded), cleanup (skip|reset|restart — the post-encode VRAM step1), error (string, only on failed) |
reindex.cleanup |
the post-reindex in-process VRAM reset finishes (only fires for cleanup:"reset") |
mode (reset), status (ok|failed), codebases, encoded, error (string, only on failed — server then restarts to recover). Await this before resuming queries; on restart the server SIGTERMs instead, so watch for it to come back |
reindex_docs.completed |
PDF/DOCX doc ingestion finishes | codebase, ingested, skipped, errors (counts) |
crawl.started |
a web crawl run starts | run_id, env, triggered_by |
crawl.completed |
a crawl run finishes successfully | run_id, total_urls, total_errors, embedded (bool) |
crawl.error |
a crawl run fails | run_id, reason |
scanner.blocked |
a file is quarantined during indexing (secret/threat) | file (path), rule_ids (list of matched rule IDs — never the secret value), llm_escalated (bool) |
chat.completed |
a chat turn finishes | session_id, codebase, source_type_filter, response_chars (length — not the text), provider, model |
tier.limit_warning |
a reindex exceeds the licensed file cap | codebase, files, file_limit, tier |
Events not in this list are ignored. The canonical list lives in
core/hooks.py(EVENTS).
Script hooks (the airgap-native channel)¶
When HOOK_SCRIPT_DIR is set, CodeNavi looks for an executable named exactly after the
event — scanner.blocked.sh, reindex.completed.sh, etc. — and runs it with the JSON
payload on stdin. Contract:
- Name =
<event>.sh(with the dot, e.g.crawl.error.sh). Only the events you write a script for fire; the rest are silent no-ops. - Input = the full envelope JSON on stdin.
- Must be executable (
chmod +x). - Timeout = 30 s by default, then the script is killed — configurable via
HOOK_SCRIPT_TIMEOUT_SEC. - Exit code and stdout/stderr are ignored — write to your own log/endpoint, not stdout.
Minimal example (scanner.blocked.sh):
#!/usr/bin/env bash
payload="$(cat)"
file="$(printf '%s' "$payload" | python3 -c "import sys,json;print(json.load(sys.stdin)['data'].get('file',''))")"
mkdir -p /tmp/codenavi-hooks
echo "$(date -Is) secret quarantined in: $file" >> /tmp/codenavi-hooks/security.log
Ready-to-use, offline-by-default examples live in examples/hooks/ in your CodeNavi package.
Use cases¶
- Security / compliance audit feed. Forward
scanner.blockedto a local SIEM or ticketing system — an auditable record that CodeNavi detected and quarantined a secret, with no secret value in the event. See the security model. - Index-freshness automation. React to
reindex.completedinstead of polling — refresh a dashboard, run an impact report, or release a downstream job once the index is current. - Ops alerting. Route
crawl.error, afailedreindex.completed, ortier.limit_warningto your alerting stack. - Usage metering. Tally
chat.completedfor internal chargeback or seat reporting (metadata only).
Because the script channel is just "run my script," your team wires CodeNavi into existing internal tooling with a few lines — no vendor-built connectors required.
Operational notes¶
- Delivery is best-effort: one attempt, no retry, no persistence. If your receiver is down, the event is dropped (a warning is logged).
- Hooks run on daemon threads and never block or fail indexing/chat.
- The only event that can fire in bulk is
scanner.blocked(one per quarantined file); in practice quarantined files are rare.
-
Vendor change reference CN-281. ↩