User-space host quarantine and static file triage daemon.
quarantined watches the directories where risky files land (~/Downloads, /tmp, AUR helper
caches), statically analyses new executables and PKGBUILD build scripts without ever
executing them, scores them, and atomically isolates high-confidence threats in a quarantine
vault. A Bubble Tea TUI provides live telemetry, vault browsing, file inspection and safe
restore.
Written in Go. No root, no kernel module, no LD_PRELOAD.
Phase 6 complete (validation corpus + end-to-end suite). Version 0.1.0-dev, pre-release.
This is a reactive, user-space daemon. It observes filesystem events and acts after a file
appears. It cannot block a write/exec pair synchronously the way an in-kernel EDR can, and it
does not see processes that never touch a watched directory. It is a staging-area containment
layer, not a complete endpoint agent. Kernel-level synchronous pre-execution blocking is a
separate design (PLAN_KERNEL_EDR.md, not in this repository).
fsnotify watcher (recursive, debounced)
|
v
Ingestion pipeline -> Static triage -> Multi-factor score
| |
ELF / PE / Mach-O < 50 PASS (log only)
Shannon entropy 50-84 ALERT (TUI)
PKGBUILD full AST >= 85 QUARANTINE
|
v
Quarantine vault -> IPC server (Unix socket, SO_PEERCRED) -> TUI client
- Watcher (
internal/watcher) — recursivefsnotify; new subdirectories get their own watch before files are written.IN_MOVED_TOandPKGBUILDcreation trigger immediately, binary payloads are debounced 300ms onIN_CLOSE_WRITE. Queue overflow triggers a full directory walk reconciliation. - Safe file access (
internal/fsutil) —OpenSafeusesO_NOFOLLOW|O_CLOEXEC; reads are bounded (ReadBounded, 64MB cap) so a hostile file cannot exhaust memory. - Binary triage (
internal/triage/binary) — ELF, PE and Mach-O parsers. Signals: writable and executablePT_LOADsegment (+40), executableGNU_STACK(+35), high section entropy, packer/encryption heuristics (+25), IOC strings such asVirtualAllocEx(+20), word-bounded non-private IPs (+35). - PKGBUILD triage (
internal/triage/pkgbuild) — full AST parse viamvdan.cc/sh/v3/syntax, covering root scope, subshells, every function (pkgver,prepare,build,check,package) and*.installscriptlets. Detects remote execution (curl ... | sh,eval,base64 -d,/dev/tcp), writes outside packaging staging variables (/etc/sudoers,~/.ssh,~/.bashrc) and untrustedsource=()hosts. VCS packages are exempt fromSKIP-checksum penalties; legitimate forge and mirror hosts are allowlisted. - Quarantine vault (
internal/quarantine) — atomic cross-mount move into~/.local/share/quarantined/vault/, original artifact locked withfchmod 0000(POSIX) or a deny-all DACL (Windows), plus a<sha256>.meta.jsonsidecar recording SHA-256, original path, original mode, uid/gid and triggering signals.PKGBUILDartifacts are additionally replaced in place by a stub that exits non-zero, somakepkgcannot proceed. - Desktop notifications (
internal/notify) — FreeDesktop desktop notification popups vianotify-sendor D-Bus (busctl). Critical alerts on file quarantine, warning popups on threat alerts, with 3-second deduplication and non-blocking asynchronous dispatch. - IPC (
internal/ipc) — newline-delimited JSON RPC over$XDG_RUNTIME_DIR/quarantined.sock, mode0600, peer authenticated with LinuxSO_PEERCREDor macOSLOCAL_PEERCREDso only the owning UID can connect. Windows peer authentication is not supported; the daemon rejects IPC connections there. Methods:Ping,GetStatus,ListVault,InspectItem,RestoreItem,PurgeItem,AddWatchPath. - TUI (
internal/tui) — auto-reconnecting Bubble Tea client with four panes: Dashboard (live feed, counters, hunter-eye emblem), Vault, Inspector and Watch Targets.
- Go 1.26 or newer (see
go.mod) - Linux, macOS or Windows
- systemd user session (optional, for running it as a service)
curl -fsSL https://raw.githubusercontent.com/OnlyOneArthur/quarantine-daemon/main/install.sh | bashOptions can be passed via bash -s --:
# install binary only (skip systemd user service)
curl -fsSL https://raw.githubusercontent.com/OnlyOneArthur/quarantine-daemon/main/install.sh | bash -s -- --no-service
# custom install prefix
curl -fsSL https://raw.githubusercontent.com/OnlyOneArthur/quarantine-daemon/main/install.sh | bash -s -- --prefix ~/.localgit clone https://github.com/OnlyOneArthur/quarantine-daemon.git
cd quarantine-daemon
./install.shThe installer builds the binary into ~/.local/bin/quarantined and, unless --no-service is
given, installs and enables quarantined.service as a systemd user unit.
./install.sh [options]
--prefix DIR install prefix (default: ~/.local)
--bin-dir DIR binary destination (default: PREFIX/bin)
--ref REF git branch/tag to fetch if run remotely (default: main)
--no-service build and install the binary only
--no-enable install the unit but do not enable/start it
-h, --help show help
Environment overrides: PREFIX, BIN_DIR, DESTDIR, REPO_URL, REF.
make build
mkdir -p ~/.local/bin ~/.config/systemd/user
cp bin/quarantined bin/quarantinctl ~/.local/bin/
cp systemd/quarantined.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now quarantined# daemon in the foreground
go run ./cmd/quarantined -config demo/demo.toml
# CLI commands
go run ./cmd/quarantinctl --config demo/demo.toml status
go run ./cmd/quarantinctl --config demo/demo.toml list
# TUI in a second terminal
go run ./cmd/quarantinctl --config demo/demo.toml tui
# (or legacy: go run ./cmd/quarantined -tui -config demo/demo.toml)demo/demo.toml watches only demo/watch/ and quarantines into demo/vault/, so it is safe to
experiment with. Relative paths in a config file resolve against that file's directory, so the demo
works from any working directory.
To see a quarantine happen, drop a fixture into the watched directory while the daemon runs:
mkdir -p demo/watch/evil-pkg
cp demo/fixtures/malicious/PKGBUILD demo/watch/evil-pkg/PKGBUILDThe PKGBUILD engine only runs on a file whose basename is exactly PKGBUILD (or ends in
.install), which is why each fixture sits in its own directory. Dropping it under any other name
sends it to the binary engine instead. demo/fixtures/clean/PKGBUILD is the benign counterpart
and passes.
quarantined [-config path/to/config.toml] [-log-level debug|info|warn|error]
quarantined -version
quarantined -tui [-config path/to/config.toml]
quarantinctl [global options] <command> [command options] [arguments]
Commands:
status Show daemon running status and statistics
list List all quarantined items in the vault
inspect <id> Show detailed threat report for a quarantined item
restore <id> [--to P] Restore an item to its original path or destination
purge <id> Permanently delete an item from the vault
watch add <path> Dynamically add a directory to watch list
tui Launch the interactive terminal UI
version Display version information
Global Options:
--socket PATH Path to daemon UNIX socket
--config PATH Path to configuration file
--timeout DURATION Request timeout (default: 5s)
--json Output results in JSON format
Run with no -config and the built-in defaults apply: watch ~/Downloads, /tmp,
~/.cache/yay and ~/.cache/paru/clone; quarantine at score >= 85 into
~/.local/share/quarantined/vault.
| Key | Action |
|---|---|
1 / 2 / 3 / 4 |
Dashboard / Vault / Inspector / Watch Targets |
tab |
next pane |
up/k, down/j |
move selection |
g, G |
top, bottom |
pgup/ctrl+u, pgdn/ctrl+d |
page up, page down |
enter |
inspect selected item |
esc |
back to vault |
a |
add a watch path |
r |
restore selected item to its original path |
d |
purge selected item permanently |
y / n |
confirm / cancel |
? |
expanded help |
q / ctrl+c |
quit |
TOML, passed with -config. Any field left out keeps its default.
[service]
log_level = "info" # debug | info | warn | error
poll_interval_ms = 300 # debounce window for write-then-close events
socket_path = "$XDG_RUNTIME_DIR/quarantined.sock" # falls back to ~/.local/state
[paths]
watch = ["~/Downloads", "/tmp", "~/.cache/yay", "~/.cache/paru/clone"]
ignore_extensions = [".crdownload", ".part", ".tmp"]
[triage_binary]
enabled = true
entropy_threshold = 7.3
max_scan_size_mb = 64
[triage_pkgbuild]
enabled = true
block_untrusted_sources = true
flag_skip_checksums = true
[quarantine]
vault_dir = "~/.local/share/quarantined/vault"
threshold_score = 85 # >= this score is quarantined
max_vault_size_mb = 1024
retention_days = 14go vet ./...
go test -count=1 ./...11 packages are covered: cmd/quarantined, internal/config, internal/fsutil, internal/ipc,
internal/logger, internal/quarantine, internal/triage/binary, internal/triage/entropy,
internal/triage/pkgbuild, internal/tui, internal/watcher.
testdata/ holds the synthetic validation corpus: clean and malicious PKGBUILDs, ELF and PE
fixtures (executable stack, RWX segment, embedded reverse-shell IOCs), high/low entropy blobs and
benign scripts. cmd/quarantined/integration_test.go exercises the full pipeline plus every IPC
method end to end.
./uninstall.sh # stop + remove service and binary, keep the vault
./uninstall.sh --purge # also delete the vault and config
./uninstall.sh --dry-run # print what would be removedThe vault is never touched unless --purge is passed and confirmed. --purge destroys the only
copy of every quarantined file; restore what you need first with r in the TUI.

