agentless configuration over ssh

shellf_

Write down the state your servers should be in — shellf makes them match, and shows you exactly what will change before it touches anything.

Configuration as plain shell — idempotent, previewable, fast.

deploy@control ~/infra
$ shellf run --check --inventory hosts.shellf site.shellf on web: web1: apt.install(nginx) would.installed file-write(/etc/nginx/…) would.written service(nginx) would.converged $ shellf run --inventory hosts.shellf site.shellf apt.install(nginx) ok.installed file-write(/etc/nginx/…) ok.written service(nginx) ok.converged $ shellf run --inventory hosts.shellf site.shellf # re-run apt.install(nginx) ok.already service(nginx) ok.already

What it does

Packages & services

apt.install, service, files, dirs — installed, running, owned as declared.

Config, per host

template fills @{vars} from the inventory, rendered per host.

Containers

docker.compose-up, docker.network--check previews the recreate.

Firewall

ufw.open / default / enable — only the ports you list.

Files & trees

dir-copy byte-for-byte (binary-safe), file-download sha256-checked.

Raw shell

A shell { } block, gated by the same contract.

Also: secrets off disk, hash-verified modules, ssh-agent auth, for / with { }.

The stdlib is early and growing — missing an instruction? Propose a def ↗


Write a plan

hosts.shellf
defaults = { user: "deploy" }

host web1 = { address: "10.0.0.1", role: "edge" }
group web  = [web1]
site.shellf
on web {
    apt.install("nginx")
    template("nginx.conf", "/etc/nginx/nginx.conf")
    service("nginx", "true", "true")   # running, enabled

    # raw shell — still gated by the same contract
    if !shell { nginx -t } {
        service-reload("nginx")
    }
}
shellf status — current vs. desired
$ shellf status --inventory hosts.shellf site.shellf on web: web1: apt.install(nginx) installed: true file-write(/etc/…) synced: false → true service(nginx) running: false → true enabled: true

Why it beats a script

idempotent

Checks first, skips what's done. Re-run the whole plan; only drift changes.

previewable

--check shows what would change; status diffs current vs. desired. Touches nothing.

fast

One SSH connection, an agent on the host. No round-trips, no runtime.


Architecture

01 · your machine

One static binary reads your plan. The only place shellf is installed.

ssh
02 · the target

Pushed as an ephemeral agent; evaluates on the host, applies only the drift. Secrets on tmpfs.

ttl
03 · nothing left

The agent self-erases. No daemon, no state file, nothing installed.


Install

install & run
# the latest static binary (linux/amd64) $ curl -fsSL https://github.com/haribo/shellf/releases/latest/download/shellf-linux-amd64 -o shellf && chmod +x shellf # preview, apply, check state — flags before the plan $ ./shellf run --check --inventory hosts.shellf site.shellf $ ./shellf run --inventory hosts.shellf site.shellf $ ./shellf status --inventory hosts.shellf site.shellf

or build from source: go build -o shellf ./cmd/shellf