Config Guard
Config Guard is a detective-layer integrity watcher built into the ssg daemon. It computes a SHA-256 baseline for every protected AI agent config file at startup, polls for changes on a 5-second interval, records any drift to a SQLite table, emits a real-time SSE event to the dashboard, and gives you a one-click restore to the known-good content.
Detective, not preventive. Config Guard audits, alerts, and restores. It does not hard-block or deny a tool call — that is the job of your .rules files. It is tamper-evident, not tamper-proof: a userspace process cannot stop a root-capable actor, and Config Guard makes no claim that it can. It is complementary to (not a replacement for) kernel-level EDR.
Pure userspace, no elevation. The implementation uses only Go standard library calls: os.Stat, os.ReadFile, crypto/sha256, and time.Ticker. There is no eBPF, no inotify/FSEvents/ReadDirectoryChangesW, no kernel dependency, no cgo, no privilege escalation.
What gets protected
Config Guard watches the config files each supported AI agent harness reads for permissions, hooks, and MCP server lists. The catalog is fixed at startup and extended automatically when new files appear (no daemon restart needed).
| Kind | Examples |
|---|---|
settings | .claude/settings.json, .claude/settings.local.json, .codex/config.toml, .gemini/settings.json |
settings-runtime | ~/.claude.json (Claude Code runtime state — MCP command poisoning still flagged; permission-grant echoes exempted to avoid false positives) |
mcp | .mcp.json, .cursor/mcp.json, .vscode/mcp.json |
hooks | Every script under .claude/hooks/ (per-project and home-level) |
keybindings | .claude/keybindings.json |
Both per-project and home-level paths are covered. If a path does not exist at baseline time it is recorded as absent — a later creation is detectable drift.
How it works
Baseline
On daemon startup, Baseline() reads every protected path, computes a SHA-256 hash, and stores the known-good in a config_integrity SQLite table and a content-addressed blob store under ~/.sigmashake/configguard/blobs/. A path already in the database whose content has not changed keeps its existing record (including tamper history) — restarting the daemon does not silently re-accept a change made while it was stopped.
Poll loop
A single background goroutine ticks every 5 seconds. Each tick:
- Stats every path cheaply (
os.Stat— size, mtime, mode). - Re-hashes only the paths where something moved (the rare path).
- If the hash differs from the known-good baseline, records a drift event.
A benign mtime touch with an unchanged hash does not trigger a drift event.
Drift classification
Each drift event carries a kind and a severity:
| Kind | Meaning |
|---|---|
modified | Content changed |
created | A path baselined as absent now exists |
deleted | A present path disappeared |
permission | File mode changed, content unchanged |
Severity is warning for ordinary edits and critical for config-poisoning signatures (remote-fetch piped to an interpreter, base64-decode piped to exec, permission allow-all grants). Only unresolved critical drift triggers the red "tamper" status — ordinary edits stay amber "attention" so the dashboard does not alarm on normal developer work.
SSE and audit
On every drift detection the daemon:
- Broadcasts a
config-integritySSE event to the dashboard. - Writes an
audit_logrow withTool="ConfigGuard"andDecision="log"(never a block).
Git-FIM layer (enhancement)
When git is available on the host, Config Guard maintains a local-only git repository at ~/.sigmashake/configguard/fim/ that mirrors every baseline and drift as a commit. This gives you a real commit log and unified diffs in the dashboard history panel. When git is absent the blob store is the floor — detection and restore work without it.
Health statuses
The dashboard banner and the GET /api/config-guard/health endpoint report one of five statuses:
| Status | Meaning |
|---|---|
protected | Polling, baseline present, no unresolved drift |
attention | One or more unresolved warning-level drifts (amber — ordinary edits) |
tamper | One or more unresolved critical-severity drifts (red — attack-shaped changes) |
stale | Enabled but no completed scan in the last 30 seconds |
disabled | Protection off or nothing baselined |
Dashboard Config Guard page
Open the Config Guard page from the dashboard sidebar to see:
- A protection status pill (green/amber/red).
- A roster of every protected file with its state (
baselined,absent, ordrift), last-check time, baseline hash, current hash, and drift count. - A version history panel per file showing every content version that was captured, each with a short hash and the option to restore to that specific version.
- Unified diffs between any two versions (when git is available).
- Restore, quarantine, and accept-changes actions.
Actions
Restore to baseline
Reverts a protected file to its original known-good content. The write is atomic (temp file + rename) so a crash mid-write never leaves a truncated config. The known-good pointer stays at the original baseline.
Restore to a specific version
Any version in the history panel whose content blob was captured can be restored. After restore that version becomes the new known-good anchor (git-checkout semantics) while the full history is preserved.
Accept changes
Adopts the current on-disk content as the new known-good baseline. Use this when a drift was intentional (you edited your own settings). The prior versions remain in history and are still restorable.
Quarantine
Moves the drifted file aside to a timestamped copy in a .configguard-quarantine/ directory next to the original, preserving the evidence. By default the baseline is restored in its place immediately after the move.
HTTP API
All routes require the dashboard bearer token (X-SigmaShake-Token header or Authorization: Bearer). Memory Guard exposes the same set under /api/memory-guard/*.
| Method | Path | Description |
|---|---|---|
GET | /api/config-guard/health | Typed health snapshot (enabled, protectedFileCount, lastScanAt, status, driftOpenCount, driftCriticalOpen, driftLast24h, lastEventAt) |
GET | /api/config-guard/files | Per-path protection roster (state, severity, driftKind, baselineHash, currentHash, tamperCount) |
GET | /api/config-guard/enabled | Read the on/off toggle |
POST | /api/config-guard/enabled | Set the on/off toggle. Body: {"enabled": true} |
GET | /api/config-guard/mode | Read the enforcement mode (detect or prevent) |
POST | /api/config-guard/mode | Set the enforcement mode. Body: {"mode": "detect"} |
POST | /api/config-guard/scan | Force an immediate integrity check |
GET | /api/config-guard/history | Version lineage for one path. Query: ?path=<abs> |
GET | /api/config-guard/diff | Unified diff between two refs. Query: ?path=<abs>&from=<ref>&to=<ref> |
POST | /api/config-guard/restore | Restore a path. Body: {"path": "<abs>", "hash": "<sha256 or empty for baseline>"} |
POST | /api/config-guard/accept | Accept current content as new baseline. Body: {"path": "<abs>"} |
POST | /api/config-guard/quarantine | Quarantine the drifted file. Body: {"path": "<abs>", "restore": true} |
Enforcement mode
The mode field has two values:
detect(default) — audit, alert, and restore only. No tool call is ever blocked by Config Guard itself.prevent— the rule engine activates an off-by-default DENY preset that blocks agent writes to protected paths. Config Guard stores and exposes this mode; the.rulesengine enforces it viaPreToolUse.
The default is detect. prevent mode is off unless you explicitly enable it from the dashboard or the API.
On/off toggle
Config Guard is enabled by default. You can turn it off from the dashboard Config Guard page or via POST /api/config-guard/enabled. The choice persists across daemon restarts in ~/.sigmashake/configguard/state.json. When off, the poll loop is a no-op and health reports disabled — but captured baselines and version blobs are retained so re-enabling is instant and no history is lost.
Limitations
- Userspace only. A sufficiently privileged process can stop the daemon or modify the blob store. Config Guard detects changes made while it is running; it cannot protect against tampering while it is stopped.
- Restore requires captured content. A path can only be restored to a version whose byte-exact content blob was captured. Versions observed before blob capture was added show
canRestore: falsein the history. - Restore to baseline for an absent path means deletion. If a file was baselined as absent (did not exist at baseline time) and later appeared, restoring to baseline removes the file — that is the correct known-good.
- No kernel filesystem watch. The poll interval is 5 seconds. A change made and reverted within a single poll tick is not detected.