SIS Guardian

SIS Guardian — install, capabilities, and configuration (macOS and Linux)

The guardian is the local, always-on side of sis: it watches folders you nominate, scans what arrives, raises a desktop alert, and — when you ask it to — moves a bad arrival into a hash-verified quarantine vault.

This document covers running it from the command line on macOS and Linux. It states what the build actually does today, including where a capability is thinner on one platform than the other; the “Platform reality” table is the honest version and the daemon reports the same facts at runtime via sis-guardian --paths.


1. What ships

Three binaries participate, and they are separate deliberately.

BinaryWhat it isWhen you want it
sisthe analysis CLI — scan, query, watch, sanitize, assess, cdr-gatewayone-shot analysis, and single-folder watch pipelines
sis-guardianthe long-running daemon — multi-folder watch, SQLite history, alerts, quarantine vault, audit trailan always-on local guardian
sis-guardian-mcpthe MCP server, over the same databaseletting an agent query alerts / scans

sis watch and sis-guardian overlap but are not the same tool. The short version:

  • sis watch <DIR> — one directory per process, rich action vocabulary (quarantine, CDR-sanitise, AI-safe-text, JSONL audit, webhook), no persistent database, no desktop notification (its notify action prints a line to stdout). Deprecated: it is a second watcher product with its own policy vocabulary and its own vault, and is scheduled for removal once the daemon covers the CDR-mirror workflow (design/20260803-guardian-local-automation-reconciliation.md, Stage H). It still works today and scenario 2 still uses it — do not build on it.
  • sis-guardian — many directories from one config, persistent history and alert store, real desktop notifications, hash-verified quarantine vault with restore and audit, optional MCP surface. Its action vocabulary is narrower: notify, plus quarantine as an explicit operator step. Unattended quarantine is disabled — see §6.

Scenario 1 below wants the daemon. Scenario 2 wants sis watch.

2. Install

From a release

The published installer (see the README) fetches released binaries:

curl -fsSL https://raw.githubusercontent.com/payloadglass/sis-release/main/scripts/install.sh | sh

Check what you got with sis --version and sis-guardian --help.

Verify the install

sis-guardian --paths
{
  "config_path": "/home/you/.config/sis/config.toml",
  "downloads": "/home/you/Downloads",
  "platform": "os=linux watcher=yes notifications=yes notification_actions=yes provenance=yes",
  "quarantine_dir": "/home/you/.local/state/sis/quarantine",
  "reports_dir": "/home/you/.local/state/sis/reports",
  "state_dir": "/home/you/.local/state/sis"
}

The platform line is generated from the build you are actually running — read it rather than assuming, particularly for notifications (off in a --no-default-features build) and notification_actions.

3. Where things live

Resolved through the platform conventions, not hard-coded. Always confirm with sis-guardian --paths.

LinuxmacOS
config~/.config/sis/config.toml~/.config/sis/config.toml
state root~/.local/state/sis~/Library/Application Support/sis
database<state>/sis.db<state>/sis.db
quarantine vault<state>/quarantine<state>/quarantine
reports<state>/reports<state>/reports

macOS deliberately keeps the config at the XDG-style ~/.config/sis/config.toml so the sis CLI and the guardian share one file, matching the CLI’s documented default. State follows the macOS convention because it is machine-local data, not user-editable configuration.

4. Platform reality

CapabilityLinuxmacOSNotes
daemon runsyesyesone runtime, no per-OS gate
folder watchingyes (inotify)yes (FSEvents)recursive; both write and move-in are scanned
desktop notificationyes (D-Bus)yes (NSUserNotification)needs the notifications feature (on by default)
notification urgencyyesignoredmacOS has no urgency channel
notification action buttonsyesno”Quarantine” / “Open report” ride the freedesktop ActionInvoked signal
automatic quarantineyesyesconfig-driven; does not depend on notification actions
quarantine + restore + audityesyes
download provenance (observed_in_downloads)yesnothe collector has a Linux adapter only
autostart registration helperyes (systemd --user)nowrite a launchd plist by hand — see §8
MCP serveryesyes

The macOS gaps that matter for the scenarios below are notification action buttons and autostart. Neither blocks the scenarios: automatic quarantine is driven from config, not from a button click, and a launchd plist is four lines of XML.

5. Capabilities

Watching. Recursive from each configured root. Symlinks are not followed, and a path that resolves outside its watched root is dropped. A file is scanned once it has settled — its size and mtime hold steady across samples and it opens for read — so a half-written download is never scanned mid-flight. Partial-download temp extensions are skipped outright. Per-folder queue depth is bounded at 50 with oldest-drop, and a watcher overflow (kernel queue exhaustion) is recovered by re-registering the whole root through the same settle gate rather than being silently lost.

One thing worth knowing: files already present when the daemon starts are not scanned. The daemon watches for arrivals. To sweep what is already there, run sis watch <DIR> --once or sis scan.

Scanning. Every scan runs through the same engine the CLI uses, at one of three presets: Quick, Standard, Deep. Format is detected from content, never from the extension.

Alerting. Suspicious and Malicious raise a desktop notification; Clean, Anomalous and Error stay silent so the daemon is not noisy. Repeats of the same file+verdict are suppressed within a 60-second window.

Quarantine. A move into the vault, Blake3-hashed before and verified after, recorded in the database with its original path, and written to the destructive-action audit trail. The vault directory is created with restrictive permissions. Restore puts the file back at its original path and is never automatic — a restore never opens the content.

History and audit. Every scan, alert, and destructive action is persisted. retain_days prunes scan history at startup.

MCP. With --mcp the daemon hosts an MCP server on stdio over the same live service: sis.status, sis.scan_path, sis.list_alerts, sis.get_alert, sis.list_scans, sis.get_scan, sis.list_watched_folders, sis.update_watched_folders, sis.quarantine_file, sis.restore_file, sis.list_audit, plus the AI-ingestion tools. The destructive tools are gated by [guardian.mcp] — see §7.

Command surface

sis-guardian --foreground [--watch-downloads] [--mcp]   run the daemon
sis-guardian --init-config                              write a starter config.toml
sis-guardian --paths                                    resolved paths + platform capabilities
sis-guardian --list-watched                             folders from config
sis-guardian --status                                   monitors, job count, alert count
sis-guardian --scan PATH                                scan one file now (never moves it)
sis-guardian --recent-alerts | --recent-scans           history
sis-guardian --list-quarantine                          what is in the vault
sis-guardian --restore QUARANTINE-ID                    put a file back
sis-guardian --audit                                    destructive-action trail

Every one-shot prints JSON to stdout, so they compose with jq.


6. Scenario 1 — watch ~/Documents and ~/Downloads, notify, quarantine on your say-so

Unattended quarantine is disabled. An earlier version of this document described [guardian] auto_quarantine as a working setup. It is not available in a released build, and a default daemon refuses to start while the key is set. Two defects make an unsupervised move unsafe: the action is bound to a pathname rather than to the bytes that were scanned, so a file replaced between the scan and the move is quarantined on another file’s verdict; and the vault moves the file before it records the move, so a failure part-way can leave the file with no restorable record. Both are being fixed — design/20260803-guardian-local-automation-reconciliation.md, Stages C and E.

What works today: watch, scan and alert, with quarantine as an explicit step. That is the setup below. It is the same protection minus the unattended move.

Use the daemon.

sis-guardian --init-config
$EDITOR ~/.config/sis/config.toml

The generated file already contains your real folder paths as commented examples. Make it look like this:

[guardian]
retain_days = 30

[[guardian.folders]]
path = "/home/you/Documents"     # macOS: /Users/you/Documents
preset = "Standard"

[[guardian.folders]]
path = "/home/you/Downloads"     # macOS: /Users/you/Downloads
preset = "Standard"

Run it:

sis-guardian --foreground

What happens when a document lands in either folder — whether it is written there or moved there:

  1. it settles, then it is scanned at the Standard preset;
  2. Clean → nothing, silently;
  3. Suspicious → a desktop notification, file left alone;
  4. Malicious → a critical desktop notification, file left alone.

Then you decide. Read the alert, and quarantine if you want to:

sis-guardian --recent-alerts | jq '.[] | {file_name, verdict, score}'
sis-guardian --scan ~/Downloads/the-file.pdf     # full detail; never moves the file

On Linux the notification also carries a Quarantine button, which does the same thing with one click. macOS notifications render no buttons (§4), so there the CLI is the route.

Inspect and undo the vault:

sis-guardian --list-quarantine
sis-guardian --restore <id-from-list-quarantine>
sis-guardian --audit

Note that restoring a still-malicious file into a folder the daemon watches will get it scanned and alerted again — restore to somewhere unwatched if you want to keep it quietly.

What you give up, and what to do instead

Without the unattended move, a malicious file sits in your Downloads folder until you act on the alert. If that gap matters for your threat model, the honest options today are:

  • treat the critical notification as the trigger and quarantine within the same session;
  • keep the watched folder somewhere you do not casually open files from;
  • for an intake pipeline rather than a personal folder, use scenario 2 — a CDR mirror never leaves the original where anything will open it, and its safety does not depend on a destructive move.

Bounding what the daemon may touch

allowed_roots refuses destructive file operations outside the listed roots. It bounds the MCP/agent surface today, and it will bound automatic containment when that returns:

[guardian.mcp]
allowed_roots = ["/home/you/Documents", "/home/you/Downloads"]

macOS specifics

  • Grant the terminal (or the daemon binary) access to Documents/Downloads the first time macOS prompts, under System Settings → Privacy & Security → Files and Folders. Without it the watcher registers but never sees events.
  • Notifications appear in Notification Centre. The “Quarantine” / “Open report” buttons will not be there — that is the freedesktop-only capability from §4. Quarantine from the CLI instead; the daemon’s alert tells you which file and why.

7. Scenario 2 — mirror a safe extract of ~/Inputs into ~/Inputs-Extract

This one is sis watch, not the daemon: the daemon’s action vocabulary is notify/quarantine, while sis watch carries the CDR and safe-text actions.

There are two different things “safe extract” can mean, and they produce different artefacts. Pick the one that matches what you are going to do with the output.

(a) A disarmed copy of the document (CDR)

Same format in, same format out, with the active content removed — macros, scripts, launch actions, embedded objects. Use this when a human or an application still has to open the file.

mkdir -p ~/Inputs ~/Inputs-Extract
sis watch ~/Inputs \
  --on benign=sanitise \
  --sanitise-dir ~/Inputs-Extract \
  --jsonl-out ~/Inputs-Extract/audit.jsonl

benign is the lowest threshold, so this fires on every file, which is what “mirror everything that lands here” means. To disarm only what is actually risky, use --on suspicious=sanitise.

Output is named <sha8>-<original-name>.sanitised in the target directory — the hash prefix keeps two same-named inputs from colliding. Formats that can be sanitised today are PDF, OOXML, RTF, HTML and archives; anything else is skipped with a line on stderr, so check that log rather than assuming an absent output means “nothing to remove”.

(b) Model-safe text (AI ingestion)

The document’s text with hidden instruction channels stripped, plus a log of what was removed. Use this when the destination is an LLM or an index rather than a person.

sis watch ~/Inputs \
  --on ingest_unsafe=safe-text \
  --safe-text-dir ~/Inputs-Extract \
  --jsonl-out ~/Inputs-Extract/audit.jsonl

Writes <stem>.safe.txt and <stem>.removals.json. Note this fires only for documents that reach the hidden_instruction ingestion-risk band — it is a remediation action, not an unconditional extractor. For unconditional text extraction of everything, sis extract per file is the direct route.

Both at once

The two axes are independent and compose in one process:

sis watch ~/Inputs \
  --on benign=sanitise --sanitise-dir ~/Inputs-Extract \
  --on ingest_unsafe=safe-text --safe-text-dir ~/Inputs-Extract/safe-text \
  --on malicious=quarantine --quarantine-dir ~/Inputs-Quarantine \
  --jsonl-out ~/Inputs-Extract/audit.jsonl

Draining what is already there

sis watch reacts to arrivals like the daemon does. To process a backlog once and exit:

sis watch ~/Inputs --once --on benign=sanitise --sanitise-dir ~/Inputs-Extract

Known rough edges

  • sis watch takes one directory per invocation. The [watch].folders key exists in the config schema but is not yet read by the watch command — run one process per directory.
  • Its notify action prints to stdout; it does not raise a desktop notification. If you want both an OS alert and a CDR mirror on the same folder, run the daemon on it (Scenario 1) and a sis watch --on benign=sanitise alongside, writing to a directory the daemon does not watch.
  • The webhook: action logs its intent but does not currently send HTTP.

8. Running it at login

Linux — systemd --user

mkdir -p ~/.config/systemd/user
cat > ~/.config/systemd/user/sis-guardian.service <<'EOF'
[Unit]
Description=SIS Guardian local agent

[Service]
ExecStart=%h/.cargo/bin/sis-guardian --foreground
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target
EOF
systemctl --user daemon-reload
systemctl --user enable --now sis-guardian
journalctl --user -u sis-guardian -f

macOS — launchd

cat > ~/Library/LaunchAgents/com.sis.guardian.plist <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
  <key>Label</key><string>com.sis.guardian</string>
  <key>ProgramArguments</key>
  <array><string>$HOME/.cargo/bin/sis-guardian</string><string>--foreground</string></array>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><true/>
  <key>StandardErrorPath</key><string>$HOME/Library/Logs/sis-guardian.log</string>
</dict></plist>
EOF
launchctl load ~/Library/LaunchAgents/com.sis.guardian.plist
tail -f ~/Library/Logs/sis-guardian.log

macOS will prompt for Documents/Downloads access on the first run; grant it or the watcher sees nothing.


9. Exposing the daemon to an agent

sis-guardian --foreground --mcp

stdout carries JSON-RPC and all logging goes to stderr, so it drops straight into an MCP client config.

Set [guardian.mcp] before you do this. An absent section leaves the runtime unrestricted — an agent gets quarantine, restore and watch-reconfiguration with no root bound and no confirmation. That default is a known gap, not an intention: it should be deny-by-default, and becomes so when the automation config model lands (design/20260803-guardian-local-automation-reconciliation.md §10.5, Stage F). Until then the section is not optional for any daemon exposed to an agent.

A config file that exists but does not parse can no longer reach this state: the daemon exits rather than falling back to defaults, which was the silent route into unrestricted agent access.

Configuring the section switches it to deny-by-default — only what you enable is allowed:

[guardian.mcp]
allow_quarantine = true
allow_restore = false
allow_watch_reconfigure = false
allowed_roots = ["/home/you/Downloads", "/home/you/Documents"]
require_confirmation = true

Two things this section does not restrain, on purpose:

  • your own [guardian].folders still apply at startup — that is your config, not an agent request, so allow_watch_reconfigure = false restrains the agent without stopping the daemon from watching what you told it to;
  • sis-guardian --restore still works — the operator’s undo must not be revocable by the policy whose job is to restrain agents.

allowed_roots bounds every destructive file operation the policy governs, so an agent action can never reach outside the roots you name.


10. Troubleshooting

SymptomCause
nothing to do (configure folders, --watch-downloads, and/or --mcp)no [[guardian.folders]] and no --watch-downloads
daemon logs configured watch folder is missing … skippingthe path does not exist or is not a directory; a stale entry is skipped, never fatal
nothing is scanned on macOSPrivacy & Security → Files and Folders access not granted
no notification appearsbuilt --no-default-features, or no notification daemon running — check the notifications= field of sis-guardian --paths
notification has no buttons on macOSexpected; see §4
will not perform unattended quarantine and the daemon exits 2expected — auto_quarantine is disabled and deprecated (§6); remove the key
parse …/config.toml and the daemon exits 2an existing config that does not load is fatal rather than falling back to defaults; fix the file or move it aside
files already in the folder are ignoredby design — use sis watch <DIR> --once or sis scan to sweep a backlog
sanitise: format 'unknown' not sanitisableCDR covers PDF, OOXML, RTF, HTML, archives

Turn up logging with RUST_LOG:

RUST_LOG=info,guardian.disposition=debug,sis_guardian_core=debug sis-guardian --foreground

guardian.disposition is the target that records, per scan, which action the guardian decided on and why.