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.
| Binary | What it is | When you want it |
|---|---|---|
sis | the analysis CLI — scan, query, watch, sanitize, assess, cdr-gateway | one-shot analysis, and single-folder watch pipelines |
sis-guardian | the long-running daemon — multi-folder watch, SQLite history, alerts, quarantine vault, audit trail | an always-on local guardian |
sis-guardian-mcp | the MCP server, over the same database | letting 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 (itsnotifyaction 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.
| Linux | macOS | |
|---|---|---|
| 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
| Capability | Linux | macOS | Notes |
|---|---|---|---|
| daemon runs | yes | yes | one runtime, no per-OS gate |
| folder watching | yes (inotify) | yes (FSEvents) | recursive; both write and move-in are scanned |
| desktop notification | yes (D-Bus) | yes (NSUserNotification) | needs the notifications feature (on by default) |
| notification urgency | yes | ignored | macOS has no urgency channel |
| notification action buttons | yes | no | ”Quarantine” / “Open report” ride the freedesktop ActionInvoked signal |
| automatic quarantine | yes | yes | config-driven; does not depend on notification actions |
| quarantine + restore + audit | yes | yes | |
download provenance (observed_in_downloads) | yes | no | the collector has a Linux adapter only |
| autostart registration helper | yes (systemd --user) | no | write a launchd plist by hand — see §8 |
| MCP server | yes | yes |
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_quarantineas 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:
- it settles, then it is scanned at the
Standardpreset; Clean→ nothing, silently;Suspicious→ a desktop notification, file left alone;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 watchtakes one directory per invocation. The[watch].folderskey exists in the config schema but is not yet read by the watch command — run one process per directory.- Its
notifyaction 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 asis watch --on benign=sanitisealongside, 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].foldersstill apply at startup — that is your config, not an agent request, soallow_watch_reconfigure = falserestrains the agent without stopping the daemon from watching what you told it to; sis-guardian --restorestill 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
| Symptom | Cause |
|---|---|
nothing to do (configure folders, --watch-downloads, and/or --mcp) | no [[guardian.folders]] and no --watch-downloads |
daemon logs configured watch folder is missing … skipping | the path does not exist or is not a directory; a stale entry is skipped, never fatal |
| nothing is scanned on macOS | Privacy & Security → Files and Folders access not granted |
| no notification appears | built --no-default-features, or no notification daemon running — check the notifications= field of sis-guardian --paths |
| notification has no buttons on macOS | expected; see §4 |
will not perform unattended quarantine and the daemon exits 2 | expected — auto_quarantine is disabled and deprecated (§6); remove the key |
parse …/config.toml and the daemon exits 2 | an 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 ignored | by design — use sis watch <DIR> --once or sis scan to sweep a backlog |
sanitise: format 'unknown' not sanitisable | CDR 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.