Run commands on remote machines over WebSocket. Agents connect out to a central backend; the CLI dispatches from the other end. Managed machines need no inbound firewall rules and no key distribution.
git clone https://github.com/zexk/zctl
cd zctl
docker compose up --build -dA docker-agent registers automatically once the stack is up. Build the CLI:
pnpm --filter @zctl/cli build
OP_TOKEN=$(node scripts/gen-token.js)
zctl login --url http://localhost:3000 --token $OP_TOKENzctl machinesHOSTNAME STATUS OS ARCH LAST SEEN
──────────────────────────────────────────────────────────────────────
docker-agent online linux aarch64 3s ago
zctl exec docker-agent uptime10:42:01 up 2 min, 0 users, load average: 0.00, 0.00, 0.00
zctl logs docker-agentCOMMAND STATUS EXIT CREATED
────────────────────────────────────────────────────────────────────────
uptime completed 0 5/26/2025, 10:42:01 AM
The core also serves a small operator dashboard:
http://localhost:3000/dashboard
Paste the same operator token into the dashboard to view machines, command history, and run commands.
- Machine registration with persistent WebSocket connectivity
- JWT auth: separate operator and agent roles
- Remote command execution via
sh -c, output stored per execution - Online/offline status derived from heartbeat timestamps
- Execution history per machine
- Browser dashboard backed by the same operator API endpoints
CLI --> Core API/WS Server <-- Agents (Go)
|
PostgreSQL
| Component | Stack | Role |
|---|---|---|
| Core | TypeScript, Fastify, Drizzle, PostgreSQL | HTTP API, WebSocket gateway, JWT auth |
| Agent | Go, gorilla/websocket | Outbound WS connection, command execution, heartbeats |
| CLI | TypeScript, Commander.js | Operator CLI |
Add the flake input and import the module:
inputs.zctl.url = "github:zexk/zctl";imports = [ inputs.zctl.nixosModules.zctl ];Control plane (one host):
services.zctl.core = {
enable = true;
environmentFile = "/run/secrets/zctl-env"; # JWT_SECRET=<32+ chars>; CORS_ORIGIN=https://zctl.example.com (defaults to *)
openFirewall = true;
};PostgreSQL is provisioned automatically (database.createLocally = true by default): a local database and role are created, and the service connects via Unix socket peer auth, no password required. To use an external database instead:
services.zctl.core.database.createLocally = false;
services.zctl.core.database.url = "postgres://user:pass@host/zctl";Agents (any managed host):
services.zctl.agents.default = {
enable = true;
coreUrl = "https://zctl.example.com";
};Multiple agents per host (e.g. reaching different control planes) are supported via additional attrset keys; each becomes a separate systemd unit (zctl-agent-<name>).
CLI (operator machines):
programs.zctl.enable = true;Then authenticate:
zctl login --url https://zctl.example.com --token <operator-jwt>The flake also exposes packages.<system>.{zctl-agent,zctl-core,zctl-cli} and overlays.default for use outside NixOS.
nix develop # enter devshell (Nix flake)
pnpm install # install JS dependencies
docker compose up -d # start PostgreSQL
pnpm --filter @zctl/core db:migrate
pnpm --filter @zctl/core devpnpm typecheck && pnpm test
cd agents/go-agent && go build ./...apps/
├── core/ backend API and WebSocket server
└── cli/ operator CLI
agents/
└── go-agent/ Go agent
packages/
├── protocol/ shared message types
├── config/ env schema
└── shared/ utilities
