Overview
Fleet is a set of roomy subcommands. There is no agent framework and no daemon you cannot read: a run is one process that takes a lock, scans, applies a JSON policy, disposes, writes an audit line per step and a metrics file, and exits.
roomy fleet policy init --profile agent-host --output /etc/roomy/policy.json
roomy fleet policy check # scan, show what the policy would remove; disposes of nothing
roomy fleet policy show # print the effective policy
roomy fleet run # one run: lock → scan → evaluate → dispose → purge → audit → metrics → hub
roomy fleet serve --interval 6h # run on a schedule and serve /metrics, /healthz, /report
roomy fleet audit verify|tail # tamper-evident log
roomy fleet license show|install # edition in effect
roomy fleet metrics # print the Prometheus text a run would produce
roomy hub … # central inventory and dashboard → Hub
Policy resolution order: --policy FILE, $ROOMY_POLICY, /etc/roomy/policy.json, ~/.roomy/policy.json. When the policy names a hub-managed policy (hub.policy), that one is pulled before the run; --local-policy skips the pull.
A run, step by step
- Lock
~/.roomy/fleet.lock(pid; stale locks are reclaimed). A concurrent run exits 4. - Scan the policy's roots and categories with the effective catalog (built-in +
extraRules−disabledRules). - Evaluate: each finding is either selected or skipped with a reason:
tierNeverRemoved,tierNotAutoCleaned,ruleNeverRemoved,pathProtected,categoryNotAllowed,ecosystemNotAllowed,tooRecent,idleUnknown,tooSmall,overBudget. Largest first, so a byte budget buys the most space. - Dispose selected items with the policy's method. Default is quarantine: a rename into
~/.roomy/quarantineon the same volume with a manifest, so the run is instant and reversible. - Purge quarantine items older than
retentionDays(and, on Linux withmethod: trash, the freedesktop Trash). This is when space is actually freed. - Audit: every step above is one JSON line with the hash of the previous line.
- Metrics: written atomically to the node_exporter textfile path, and served by
fleet serve. - Report to the hub, when the policy sets
hub.url: a few-KB summary of steps 2–7 plus the audit chain head. Failure is logged and audited, never fatal.
--dry-run (or "dryRun": true in the policy) does steps 1–3 and 6–8, and logs what step 4 would have done.
Policy file reference
JSON. Every field is optional; defaults are shown. Validate with roomy fleet policy check, print the effective policy with roomy fleet policy show.
{
"schemaVersion": 1,
"name": "default", // appears in audit records and metrics labels
"scan": {
"roots": ["~"], // directories to walk for project artifacts; [] = no walk
"home": null, // home to probe for tool caches; null = current user
"categories": null, // null = all available on this platform; else a list of category keys
"ecosystems": null, // null = all
"exclude": [], // paths or globs never scanned
"extraRules": [], // extra catalog files, merged by rule id
"disabledRules": [] // rule ids switched off
},
"autoClean": {
"tiers": ["safe"], // tiers removable without a human
"categories": null, // restrict to categories (null = any); "project" is the project walk
"ecosystems": null,
"minIdleDays": 7, // 0 = no age requirement
"requireKnownIdle": false, // true = skip items whose idle time is unknown
"minBytes": 1048576,
"maxBytesPerRun": null, // stop selecting past this many bytes (largest first)
"maxItemsPerRun": null
},
"never": {
"tiers": ["warning"], // never removed by policy, regardless of autoClean
"ruleIDs": [], // e.g. ["cursor-state-backup"]
"paths": [] // prefixes or globs, e.g. ["~/.claude/projects", "*/important/*"]
},
"disposal": {
"method": "quarantine", // trash | quarantine | delete
"quarantineDir": null, // default ~/.roomy/quarantine (or $ROOMY_QUARANTINE_DIR)
"retentionDays": 7,
"purgeOnRun": true // purge expired quarantine (and Linux trash) at the end of a run
},
"audit": { "path": null }, // default ~/.roomy/audit.jsonl
"metrics": { "textfile": null, // e.g. /var/lib/node_exporter/textfile_collector/roomy.prom
"listen": null }, // host:port for `fleet serve`; default 127.0.0.1:9477
"license": { "key": null, "keyFile": null }, // also $ROOMY_LICENSE, /etc/roomy/license.key, ~/.roomy/license.key
"lockFile": null, // default ~/.roomy/fleet.lock
"hub": {
"url": null, // "http://hub.internal:9478" → report to a Roomy Hub after every run
"token": null, "tokenFile": null, // agent token; also /etc/roomy/hub.token, ~/.roomy/hub.token, $ROOMY_HUB_TOKEN
"policy": null, // name of a hub-managed policy to pull before each run
"tags": {}, // labels for the dashboard: role, team, region…
"topItems": 25, // largest findings included in the report (0–200)
"intervalSeconds": null // expected run cadence for staleness; set by `fleet serve`
},
"dryRun": false,
"dormantMonths": 24, "inactiveMonths": 6 // activity labels in reports
}
Validation rejects: a tier in both autoClean.tiers and never.tiers; unknown categories or ecosystems; negative retention or idle days; a hub.url that is not http(s); a schema version newer than the binary supports.
hub.policy is set, the policy actually run is the hub-managed one; the local file contributes only hub, license, scan.home and lockFile. See managed policies.Profiles and examples
roomy fleet policy init writes a policy from a profile. Add --hub URL [--hub-policy NAME] [--tag key=value …] to either profile to fill in the hub section.
| Profile | What it does |
|---|---|
agent-host | No project walk beyond ~, tool caches and leftovers only, safe tier idle ≥ 3 days, quarantine for 7 days, metrics to node_exporter, Claude and Codex session directories protected. |
developer | Walk ~, safe tier idle ≥ 30 days with known idle only, Trash, no automatic purge. |
Aggressive, for disposable CI runners
{ "name": "ci-runner", "scan": { "roots": ["/home/runner/work", "~"] },
"autoClean": { "tiers": ["safe", "caution"], "minIdleDays": 0 },
"disposal": { "method": "delete" } }
Conservative, models only
{ "name": "models-only", "scan": { "roots": [], "categories": ["aiTools"] },
"autoClean": { "tiers": ["safe"], "ecosystems": ["ai"], "minIdleDays": 14, "maxBytesPerRun": 50000000000 } }
Bootstrap that pulls everything from the hub
{ "hub": { "url": "http://hub.internal:9478", "tokenFile": "/etc/roomy/hub.token",
"policy": "agent-host", "tags": { "role": "agent", "team": "platform" } } }
systemd
The .deb, .rpm and .apk packages install three units. The instance name is the Unix user whose home is cleaned, because artifacts belong to the user who created them.
sudo systemctl enable --now roomy-fleet@agent.timer # oneshot every 6h, randomized ±15min
sudo systemctl enable --now roomy-fleet-serve@agent.service # long-running, serves metrics on 127.0.0.1:9477
journalctl -u roomy-fleet@agent.service
| Unit | Role |
|---|---|
roomy-fleet@.service + roomy-fleet@.timer | One fleet run every 6 hours. SuccessExitStatus=4, so a held lock is not a failure. |
roomy-fleet-serve@.service | Long-running fleet serve with the built-in metrics server. Use this or the timer, not both. |
roomy-hub.service | The central hub, see the Hub reference. |
Running as root to clean many homes works (scan.home per policy, one unit instance per user) but is rarely needed. /etc/roomy/policy.json is marked as configuration, so package upgrades never overwrite a tuned policy.
Prometheus metrics
Textfile collector (default): the policy's metrics.textfile points into node_exporter's --collector.textfile.directory. No extra port, no extra process.
Built-in server (roomy fleet serve, or metrics.listen): GET /metrics (text exposition), GET /healthz (200 when the last run had no failures), GET /report (last ScanReport as JSON).
| Metric | Type | Labels |
|---|---|---|
roomy_build_info | gauge (1) | version, catalog, edition, org |
roomy_reclaimable_bytes | gauge | tier |
roomy_reclaimable_bytes_by_category | gauge | category |
roomy_reclaimable_items, roomy_planned_bytes, roomy_planned_items | gauge | |
roomy_last_run_disposed_bytes, roomy_last_run_failures | gauge | |
roomy_disposed_bytes_total, roomy_disposed_items_total, roomy_dispose_failures_total, roomy_purged_bytes_total, roomy_runs_total | counter (from the audit log, monotonic across restarts) | |
roomy_last_run_timestamp_seconds, roomy_last_run_duration_seconds, roomy_last_run_success, roomy_dry_run | gauge | |
roomy_quarantine_bytes, roomy_quarantine_items | gauge | |
roomy_volume_total_bytes, roomy_volume_available_bytes | gauge | |
roomy_license_expiry_timestamp_seconds | gauge |
Every series carries host and policy. Alerts worth having:
roomy_volume_available_bytes / roomy_volume_total_bytes < 0.1 # disk pressure despite cleanup
time() - roomy_last_run_timestamp_seconds > 2 * 21600 # runs stopped
increase(roomy_dispose_failures_total[1d]) > 0 # something could not be removed
roomy_reclaimable_bytes{tier="safe"} > 50e9 # policy is too conservative
For fleet-wide series without per-host scraping, the hub exposes roomy_hub_* metrics; see the Hub reference.
Audit log
~/.roomy/audit.jsonl by default (audit.path). One JSON object per line, each carrying the hash of the previous one:
{"seq":42,"time":"2026-10-27T01:00:00Z","host":"agent-07","runID":"…","event":"disposed","actor":"roomy-fleet",
"edition":"fleet","policy":"agent-host","path":"/home/agent/.cache/uv","bytes":492000000,"tier":"safe",
"category":"globalCaches","ruleID":"uv-cache","method":"quarantine",
"destination":"/home/agent/.roomy/quarantine/20261027T010000-uv","prevHash":"…","hash":"…"}
Events: runStarted, licenseChecked, scanCompleted, planned, skipped, disposed, disposeFailed, purged, runFinished, hubReported, error.
hash = SHA256(prevHash ‖ record-without-hash). roomy fleet audit verify recomputes the chain and exits 1 on any edit or gap. Ship the file to your SIEM as-is; it is plain JSON Lines. roomy clean appends to the same log with actor set from --actor, so an agent's manual cleanups and the policy's automatic ones sit in one record. Records written by a dry run carry dryRun: true; their disposed events describe what would have been removed and are not counted in totals, metrics or the hub.
roomy fleet audit tail -n 20
roomy fleet audit verify # exit 0 = chain intact
Licensing
Without a license every fleet command works in the community edition, and metrics and audit records say so. A fleet license is an offline Ed25519 token that sets the edition, records the organisation and host allowance, and is what commercial support is attached to.
| Community | Fleet | |
|---|---|---|
roomy scan/clean/purge/restore/rules/doctor | ✓ free | ✓ |
roomy fleet commands (policy runs, serve, audit, metrics) | ✓ work, labelled edition="community" | ✓ labelled fleet, org and host allowance recorded |
roomy hub (inventory, dashboard, managed policies, fleet metrics) | ✓ up to 5 hosts | ✓ up to the license's maxHosts |
| Commercial support, SLA, security contact | — | ✓ |
Tokens
A token looks like ROOMY1.<payload>.<signature>. The payload is JSON: id, org, edition, maxHosts, issued and expiry dates, features. Verification is offline against a public key compiled into the binary; no network call is ever made. An expired or malformed token falls back to community with the reason shown by roomy fleet license show and roomy doctor.
Where Roomy looks, in order: the policy's license.key, the policy's license.keyFile, /etc/roomy/license.key, ~/.roomy/license.key, $ROOMY_LICENSE.
roomy fleet license install ROOMY1.… # writes ~/.roomy/license.key, mode 0600
roomy fleet license install ROOMY1.… --path /etc/roomy/license.key # system-wide
roomy fleet license show
maxHosts is enforced by the hub: a report from a host it has not seen before is refused once the inventory holds maxHosts hosts (5 without a license). Hosts already in the inventory keep reporting, so an expired license degrades to a warning, not an outage. To get a license, write to us.
Rollout advice
- Start with
"dryRun": truefor a week. Readroomy fleet policy checkand theskippedreasons; tuneminIdleDays,never.paths,disabledRules. - Switch to
quarantinewithretentionDays: 7. Watchroomy_quarantine_bytes; space is reclaimed on purge. - Only then consider
method: deletefor throwaway hosts, or addingcautiontoautoClean.tiersfor specific categories. - With more than a handful of hosts, run a hub, push the policy there once, and let the hosts pull it.
warning tier: session transcripts, archives, volumes) stay in never.tiers by default. A policy can take them out, but no profile does, and the hub dashboard shows them as "not allowed by policy" rather than as reclaimable.Home · Mac app · CLI · Hub · Fleet & Hub overview · Privacy · EULA · Source on GitHub
