Offline Mode and Air-Gapped Deployment
This page documents what the ssg CLI and daemon do when there is no network,
what degrades, and how you can confirm zero egress during evaluation on your own
machine. Every claim on this page is derived directly from the shipped source.
What works fully offline
Rule evaluation is fully local and does not depend on the network. The ssg
eval engine (pure-Go in-process, go/internal/goengine) loads rules from
the local SQLite database at ~/.sigmashake/rules.db and evaluates every tool
call in-process. No external binary or shared library is required — the Go engine
runs entirely within the ssg process with no subprocesses on the eval hot path.
Nothing in this chain requires a network call. A DENY, ALLOW, or ASK decision is computed locally and returned to the calling agent before any network I/O could occur.
What you can verify yourself. With the daemon running, run a dry-run probe with all network interfaces disabled (or while capturing traffic — see the self-test section below):
ssg probe hook --tool=Bash --input='{"command":"ls -la"}'
The command returns a governance decision. If it returns a decision, local eval is working. If the binary itself is unavailable, see the degraded-mode section.
The daemon (ssg daemon) runs fully offline. It loads rules from disk,
evaluates tool calls over a Unix socket, and writes audit rows to local SQLite.
None of those operations touch the network.
The dashboard (ssg serve) serves the governance UI at 127.0.0.1:5599.
The dashboard SPA and all its assets are embedded in the binary. Loading the
dashboard does not make external network calls. The dashboard itself reads state
from the local daemon over loopback, so it works offline as long as the daemon
is running.
What makes background network calls and when
The daemon runs several background tickers. All are fire-and-forget and non-fatal — a network failure never blocks or fails an evaluation.
License heartbeat (licensed tier only)
- Endpoint:
https://accounts.sigmashake.com/api/v1/license/heartbeat - Cadence: every 6 hours
- Payload fields:
jwt(your license JWT),version,daily_eval_count - Trigger condition: only fires when a license JWT exists at
~/.sigmashake/license.jwt(i.e., you have a Pro or Enterprise license) - What happens offline: the POST fails silently. The daemon continues with the last successfully verified tier.
Tier behavior when heartbeat cannot reach the server. The license JWT
already contains your tier claim. The daemon caches the plan in memory and re-
reads the JWT on restart. As long as the JWT has not expired, your tier is
preserved locally without a successful heartbeat. When the JWT is within 7 days
of expiry, the daemon attempts a proactive refresh on the same heartbeat tick;
if the refresh fails because the network is unreachable, the JWT remains on disk
and the expiry grace period (LICENSE_EXP_GRACE_MS = 7 days) continues to apply.
Revocation requires a successful heartbeat response with status: "revoked" to
take effect — it is never applied speculatively offline.
Usage heartbeat (unlicensed / all tiers)
- Endpoint:
https://accounts.sigmashake.com/api/v1/usage/heartbeat - Cadence: approximately every 2 minutes (120 s ± 10 % jitter), deduplicated on unchanged count + date
- Payload fields:
fingerprint,version,daily_eval_count - Trigger condition: fires from all installs (licensed and unlicensed) to
keep the accounts dashboard "Today's Evaluations" tile current; skipped when
daily_eval_countis 0 or unchanged since the last successful send - What happens offline: the POST fails silently. Eval is unaffected.
Telemetry (opt-in, default OFF)
- Endpoint:
https://hub.sigmashake.com/api/telemetry - Default state: disabled. On first
ssg eval, the CLI prompts on/dev/tty: "Help improve ssg? Send anonymous usage data to the team. [y/N]". The default answer is No. Non-interactive environments (CI, piped stdin) default to opted out. The consent state is saved at~/.sigmashake/telemetry.json. - Payload fields when opted in:
event,install_id(random UUID, not your account identity),version,os,arch,decision,tool_type,rule_id— never tool input content, file paths, or your JWT. - Cadence: fire-and-forget on each eval, 2-second timeout.
- What happens offline: the POST is dropped silently.
To confirm your current opt-in state:
cat ~/.sigmashake/telemetry.json
# opted_in: false means no telemetry is sent regardless of network state.
To opt out explicitly:
# Edit the file and set opted_in to false, or delete the file to be re-prompted.
Audit sync (Pro/Enterprise only, when configured)
- Endpoint (hub):
https://hub.sigmashake.com/api/v1/audit/sync - Endpoint (fleet):
https://<fleet-host>/api/v1/orgs/<org>/audit/ingest - Endpoint (override):
SSG_SYNC_ENDPOINTenv var - Cadence: every 5 minutes
- Trigger condition: only fires when
getRetentionDays() > 0, which is true for Pro (7-day retention) and Enterprise (90-day retention) tiers, and when a valid auth token is present. - Payload fields: batches of audit rows, each containing
timestamp,tool,capability,decision,rule_id,input_summary(truncated to 200 characters — never the full tool input),client,ssg_version. - What happens offline: the POST fails silently. Unsynced rows accumulate
locally with
synced = 0in the SQLite audit log and are retried on the next 5-minute cycle.
Update check
- Endpoint:
https://download.sigmashake.com/cli/latest/<asset>(CDN passthrough to GitHub Releases) - Cadence: once every 24 hours; skipped entirely when
SSG_NO_UPDATE_CHECK=1is set - Timeout: 3 seconds (fire-and-forget; does not block evaluation)
- What happens offline: fails silently. No upgrade prompt is shown.
Hub manifest cache
- Endpoint:
https://raw.githubusercontent.com/sigmashakeinc/rules/main/manifest.json - Trigger:
ssg hub pulland hub-related dashboard operations - What happens offline: hub pulls fail with an error. Rules already installed on disk continue to work.
What degrades when offline
| Feature | Offline behavior |
|---|---|
| Rule evaluation (eval/probe) | Fully functional — no network dependency |
| Daemon and dashboard | Fully functional |
| License tier (licensed) | Preserved from cached JWT until JWT expires |
| License heartbeat | Silent failure; does not affect eval |
| Usage counter sync | Silent failure; counter still increments locally |
| Telemetry | Silent failure (only sends if you opted in) |
| Audit cloud sync | Rows queue locally; synced when network returns |
| Hub rule pulls | Fail with an error; installed rules are unaffected |
| Update checks | Silent failure; no upgrade prompt shown |
ssg auth login | Requires network (browser OAuth / device flow) |
| Fleet policy pull | Cannot pull new policy bundle; last cached bundle applies |
Self-test: prove zero egress during evaluation
Example procedure — adapt to your environment. The commands below were not
executed in producing this page; they are provided as a reproducible template
for independent verification on your own machine. The ssg probe hook command
itself (the subject of the test) was verified by the author.
Step 1 — capture baseline traffic
Open a terminal and start a packet capture or proxy. On Linux with tcpdump:
# Capture all non-loopback TCP traffic to a file, run in background
sudo tcpdump -i any -w /tmp/ssg-offline-check.pcap 'not (host 127.0.0.1 or host ::1)' &
TCPDUMP_PID=$!
On macOS, replace -i any with -i en0 (or whichever interface has your
default route). Alternatively, use mitmproxy or any HTTPS proxy you trust —
set HTTPS_PROXY to its address before the probe command.
Step 2 — run the eval probe
ssg probe hook --tool=Bash --input='{"command":"echo hello"}'
Expected output: a governance decision (allow, deny, or ask) with no
network activity visible in the capture.
Step 3 — stop the capture and inspect
# Stop tcpdump
kill $TCPDUMP_PID
# Inspect captured packets (should show zero rows to external hosts)
tcpdump -r /tmp/ssg-offline-check.pcap -n 2>/dev/null | grep -v '^$' | head -20
If no packets appear (or only loopback packets appear), the eval made no external network calls.
Why no network calls happen during eval. The eval path reads rules from the local SQLite database, runs them through the pure-Go in-process engine, and writes the result to the local audit log. There is no IPC to a remote service or subprocess on the critical path. The background heartbeat tickers run on their own intervals and are not triggered by an eval call.
Verifying telemetry is off
# Confirm opted_in is false before the capture
python3 -c "import json; d=json.load(open('$HOME/.sigmashake/telemetry.json')); print('opted_in:', d['opted_in'])"
If opted_in is false, no telemetry POST fires regardless of what eval commands
you run.
Air-gapped installation runbook
These steps document a working installation for a machine with no internet access after the initial binary download.
On an internet-connected machine:
# 1. Install ssg on an internet-connected machine and verify it
# (subscribe at sigmashake.com/pricing first; see /getting-started for install instructions)
ssg --version
# 2. Copy the ssg binary to offline media
# The ssg binary is a single self-contained Go executable — no native library required.
# On Linux: ssg is at $(which ssg)
which ssg
On the air-gapped machine:
# 3. Place the ssg binary on PATH
# (e.g. /usr/local/bin/ or ~/.local/bin/)
# 4. Initialise rules from files you transferred
# Create ~/.sigmashake/rules/ and place your .rules files there.
mkdir -p ~/.sigmashake/rules
# 5. Run the daemon
ssg daemon
# 6. Verify eval works offline
ssg probe hook --tool=Bash --input='{"command":"ls"}'
What is unavailable on an air-gapped machine:
ssg auth logincannot complete (requires accounts.sigmashake.com)- Hub rule pulls are unavailable (requires GitHub raw content)
- License heartbeats never reach the server; the JWT tier applies as long as the JWT has not expired
- Update checks never complete
What works on an air-gapped machine:
- All rule evaluation
- The daemon and dashboard (loopback only)
- All rules you transferred and installed from disk
Honest boundary
This document describes the network behavior of the ssg CLI and daemon as
implemented in the TypeScript source (src/) and the Go runtime (go/). The
desktop applications (SigmaShake Desktop for Linux, macOS, and Windows) embed
ssg serve and the ssg binary but add their own update-check calls and
desktop-platform integrations. Those behaviors are documented in the respective
desktop app guides and are not covered here.
The claims on this page can be verified by reading the source files listed below. No claim is asserted without a corresponding code reference.
Source references for every claim on this page:
| Claim | Source file |
|---|---|
| Eval is fully local | sigmashake-gov/go/internal/goengine/engine.go — Engine / Engine.Evaluate (pure-Go in-process; no subprocess on eval hot path) |
| License heartbeat endpoint and payload | sigmashake-gov/src/commands/daemon/heartbeats.ts — startLicenseHeartbeat() |
| Usage heartbeat endpoint and payload | sigmashake-gov/src/commands/daemon/heartbeats.ts — startEvalCountPush() |
| Telemetry default-off, opt-in prompt | sigmashake-gov/src/shared/telemetry.ts — checkAndPromptTelemetry() |
| Telemetry payload fields (no input content) | sigmashake-gov/src/shared/telemetry.ts — TelemetryEvent interface |
| Telemetry endpoint and 2-second timeout | sigmashake-gov/src/shared/constants.ts — TELEMETRY_ENDPOINT, TELEMETRY_TIMEOUT_MS |
| Audit sync endpoint, cadence, payload | sigmashake-gov/src/shared/sync-client.ts — syncAuditLog(), toHubAuditEntry() |
| Audit sync gated on plan retention days | sigmashake-gov/src/shared/sync-client.ts — getSyncEndpoint() |
| Heartbeat intervals | sigmashake-gov/src/shared/constants.ts — LICENSE_HEARTBEAT_INTERVAL_MS, EVAL_COUNT_PUSH_INTERVAL_MS, SYNC_INTERVAL_MS |
| Grace period on license expiry | sigmashake-gov/src/shared/constants.ts — LICENSE_EXP_GRACE_MS = 7 days |
| Update check endpoint, 24h cadence, 3s timeout, silent failure | sigmashake-gov/src/shared/update-check.ts — scheduleUpdateCheck(), fetchPlatformLatestVersion(); sigmashake-gov/src/shared/constants.ts — UPDATE_CHECK_INTERVAL_MS, UPDATE_CHECK_TIMEOUT_MS, CDN_DOWNLOAD_URL |