Plugin-based fleet management for the devices around your house —
Fire TV sticks, NVIDIA Shields, PCs — plus the software running on them.
Last verified 2026-08-14
fleetctldiscovers devices on a subnet, captures and rebuilds Kodi profiles, and deploys them over ADB or SSH. It runs from the command line, over MCP, and through a Home Assistant integration.
fleetctl separates three things that usually get tangled together:
| Ring | Knows about | Examples |
|---|---|---|
| core | nothing device-specific | transport, inventory, discovery, artifacts, operations, workflows, audit |
| packs | what a device is | firetv, shield, steamdeck, linux_host |
| apps | software on a device | kodi |
Dependencies point inward only. An app pack never imports a device pack — its steps declare the Capability values they need (exec, files, state, ...) and the engine resolves which pack on the target actually provides them. That's what lets one Kodi build deploy to a Fire Stick and a Shield without either knowing the other exists.
flowchart TB
subgraph apps["apps/ — software on a device"]
direction LR
KODI["kodi"]
end
subgraph packs["packs/ — what a device is"]
direction LR
FIRETV["firetv"]
SHIELD["shield"]
DECK["steamdeck"]
LINUX["linux_host"]
end
subgraph core["core/ — device-agnostic kernel"]
direction LR
T["transport"]
I["inventory"]
W["workflow"]
AR["artifacts"]
O["observability"]
end
KODI -. "declares capabilities —<br/>never imports a pack" .-> packs
apps ==> core
packs ==> core
classDef a fill:#7c3aed,stroke:#5b21b6,color:#fff
classDef p fill:#ea580c,stroke:#c2410c,color:#fff
classDef c fill:#0d9488,stroke:#0f766e,color:#fff
class KODI a
class FIRETV,SHIELD,DECK,LINUX p
class T,I,W,AR,O c
Vendor packs compose a shared base rather than subclassing each other — firetv and shield both build on packs/android, steamdeck and linux_host on packs/posix. That is what keeps Amazon's bugs out of the Shield: a quirk lives as data on the pack that actually has it.
Capture reads a profile off a known-good device; build reshapes it into an artifact; deploy sends that artifact to every device that can take it. The split is structural, not stylistic — build gets a transform chain and no transport, deploy gets a transport and no transform chain — so nothing can quietly rewrite a payload on its way to a device.
flowchart LR
GOLD["gold device"] -->|capture| RAW["profile archive"]
RAW --> BUILD{"build<br/>transform chain"}
BUILD --> ART[("artifact store<br/>SMB or local")]
ART -->|deploy| D1["fire stick"]
ART -->|deploy| D2["shield"]
ART -->|deploy| D3["steam deck"]
classDef g fill:#7c3aed,stroke:#5b21b6,color:#fff
classDef b fill:#005288,stroke:#003d66,color:#fff
classDef s fill:#0d9488,stroke:#0f766e,color:#fff
classDef d fill:#ea580c,stroke:#c2410c,color:#fff
class GOLD g
class BUILD b
class ART s
class D1,D2,D3 d
Requires Python 3.12+ and uv.
git clone https://github.com/salvuswarez/fleetctl.git
cd fleetctl
uv sync --all-extras
uv run fleetctl --versionThen see docs/getting-started.md for first-run config and pointing it at a real device.
uv sync --all-extras # install everything, including dev tools
uv run pytest # tests (900+)
uv run pytest --cov=src # with coverage (gate: 90%)
uv run black src tests # format
uv run isort src tests # import order
uv run mypy # strict type checking
uv build # wheel + sdistCI runs all of the above on every push and pull request.
Config is YAML and lives under config/, which is gitignored — only *.example files are tracked. Config files hold secret references, never secret values:
artifacts:
smb:
host: 192.168.1.50
user: !ref env:SMB_USER
password: !ref env:SMB_PASS!ref env:NAME resolves against the process environment at load time and is wrapped in a Secret afterward, so a fleet.yml is safe to paste into a bug report. Full field reference: docs/configuration.md.
fleetctl runs destructive operations against real hardware: wiping app profiles, disabling system packages, deploying to every tagged device. Three things keep that honest:
- Effect classification. Every step declares itself
read,mutating, ordestructive. Policy keys off the class, not a list of names. - Protected devices. Devices can be marked off-limits for named steps via config, no code change.
- Audit trail. Every mutating or destructive command is recorded — actor, target, outcome, duration — in an append-only, hash-chained log.
Because the gate keys off the declared class rather than a name, a new step is governed the day it is written — there is no list to remember to update:
flowchart LR
S["step"] --> E{"declared<br/>effect class"}
E -->|read| RUN["runs"]
E -->|mutating| POL{"policy"}
E -->|destructive| POL
POL -->|permitted| RUN
POL -->|needs approval| APR["waits for<br/>--approve"]
POL -->|protected device<br/>or over blast radius| NO["refused"]
APR --> RUN
RUN --> AUD[("audit log<br/>hash-chained")]
classDef ok fill:#0d9488,stroke:#0f766e,color:#fff
classDef gate fill:#005288,stroke:#003d66,color:#fff
classDef bad fill:#b91c1c,stroke:#7f1d1d,color:#fff
classDef wait fill:#b45309,stroke:#7c2d12,color:#fff
class RUN,AUD ok
class POL,E gate
class NO bad
class APR wait
See docs/safety.md and SECURITY.md.
MIT — see LICENSE.
|
fleetctl Overview |
|
|