Skip to main content

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

KindExamples
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
hooksEvery 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:

  1. Stats every path cheaply (os.Stat — size, mtime, mode).
  2. Re-hashes only the paths where something moved (the rare path).
  3. 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:

KindMeaning
modifiedContent changed
createdA path baselined as absent now exists
deletedA present path disappeared
permissionFile 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-integrity SSE event to the dashboard.
  • Writes an audit_log row with Tool="ConfigGuard" and Decision="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:

StatusMeaning
protectedPolling, baseline present, no unresolved drift
attentionOne or more unresolved warning-level drifts (amber — ordinary edits)
tamperOne or more unresolved critical-severity drifts (red — attack-shaped changes)
staleEnabled but no completed scan in the last 30 seconds
disabledProtection 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, or drift), 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/*.

MethodPathDescription
GET/api/config-guard/healthTyped health snapshot (enabled, protectedFileCount, lastScanAt, status, driftOpenCount, driftCriticalOpen, driftLast24h, lastEventAt)
GET/api/config-guard/filesPer-path protection roster (state, severity, driftKind, baselineHash, currentHash, tamperCount)
GET/api/config-guard/enabledRead the on/off toggle
POST/api/config-guard/enabledSet the on/off toggle. Body: {"enabled": true}
GET/api/config-guard/modeRead the enforcement mode (detect or prevent)
POST/api/config-guard/modeSet the enforcement mode. Body: {"mode": "detect"}
POST/api/config-guard/scanForce an immediate integrity check
GET/api/config-guard/historyVersion lineage for one path. Query: ?path=<abs>
GET/api/config-guard/diffUnified diff between two refs. Query: ?path=<abs>&from=<ref>&to=<ref>
POST/api/config-guard/restoreRestore a path. Body: {"path": "<abs>", "hash": "<sha256 or empty for baseline>"}
POST/api/config-guard/acceptAccept current content as new baseline. Body: {"path": "<abs>"}
POST/api/config-guard/quarantineQuarantine 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 .rules engine enforces it via PreToolUse.

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: false in 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.