Declarative system configuration for Linux. Describe the packages, files, users, services, and scripts you want — stay-go figures out what needs to change and shows you the plan before touching anything.
Most configuration tools are either too complex (Ansible, NixOS) or too limited (shell scripts). stay-go sits in the middle: a single YAML file that captures your desired system state, with an explicit diff-then-apply loop and no server required.
- Shows a plan first — every run diffs config against the live system and asks before acting
- Idempotent — running twice produces no additional changes
- Dependency-aware — resources declare deps on each other; ordering and skip propagation are automatic
- Multi-distro — auto-detects paru, yay, pacman, apt-get, dnf, zypper, apk, xbps
- Layered config — split config across
default.yaml, per-host, and per-user files
go install github.com/rayzorben/stay-go/cmd/stay-go@latestOr build from source:
git clone https://github.com/rayzorben/stay-go
cd stay-go
make installCreate a config/ directory with a default.yaml:
packages:
- neovim
- git
- fish
users:
- username: alice
shell: /bin/fish
groups: [wheel, docker]
services:
- dockerRun it:
stay-gostay-go shows a plan and waits for confirmation:
Packages
[+] neovim
[+] git
[+] fish
Users
[+] alice
Services
[+] docker
3 to add, 1 to add, 1 to add. Apply? [Y/n]
packages:
- neovim
- git
# Inline service dependency — enables docker after the package is installed
- package: docker
services: [docker]
# Force-remove a package
- "!vim"Copy files, write inline content, or create symlinks. Sources can be local paths, git repos, or URLs.
files:
# Copy a local file
- source: ${config_root}/files/gitconfig
target: ~/.gitconfig
# Inline content
- content: |
[main]
shift = oneshot(shift)
target: /etc/keyd/default.conf
sudo: true
# Symlink
- source: ${config_root}/files/nvim
target: ~/.config/nvim
symlink: true
# From a URL
- source: https://example.com/font.ttf
target: ~/.local/share/fonts/font.ttfservices:
- docker
# User service, enable without starting
- service: syncthing
user: true
now: false
depends:
- packages: [syncthing]Scripts are tracked by file content — they re-run automatically when the file changes.
scripts:
- script: ${config_root}/scripts/install-theme.sh
sudo: true
depends:
- packages: [git]
- services: [sddm]
- folders: ["!/usr/share/sddm/themes/my-theme"] # skip if already installed
commands:
# Inline shell command, tracked by content hash
- name: refresh font cache
command: fc-cache -f
depends:
- packages: [fontconfig]groups:
- docker
- wheel
users:
- username: alice
name: Alice Smith
shell: /bin/fish
home: ~/alice
groups: [wheel, docker]Declare Distrobox containers with their own package lists and setup commands:
distrobox:
- name: dev
image: docker.io/cachyos/cachyos:latest
exports: [code, firefox]
packages:
- nodejs
- python
commands:
- name: setup dev tools
command: npm install -g typescriptVariables are declared once and referenced as ${var_name} anywhere in the config.
${config_root}, ${env:VAR}, $(command), and ~ are always available.
variables:
dotfiles: ${config_root}/dotfiles
fonts_dir: /usr/share/fonts/TTF
files:
- source: ${dotfiles}/nvim
target: ~/.config/nvim
symlink: trueSplit config across multiple files — the most specific layer wins:
config/
default.yaml # applied everywhere
hosts/mymachine.yaml # applied on mymachine only
users/alice.yaml # applied when running as alice
Secrets are stored in the secrets: block as plaintext initially. On the first run, stay-go prompts for a password, encrypts each value in-place inside the YAML file, and caches the password in the OS keyring for subsequent runs.
secrets:
tailscale_auth_key: tskey-auth-abc123 # plaintext — encrypted on first run
api_keys: # nested form — referenced as ${secrets.api_keys.github}
github: ghp_abc123After the first run the file is rewritten automatically:
secrets:
_verify: !encrypted sn+HxIu... # sentinel — verifies the correct password
tailscale_auth_key: !encrypted Zm9v... # ciphertext replaces the plaintext
api_keys:
github: !encrypted dGVzdA...Decrypted values are available as ${secrets.name} (or ${secrets.group.name} for nested keys) anywhere in the config at execute time. They are never shown in plan output.
files:
- content: ${secrets.tailscale_auth_key}
target: /etc/tailscale/authkey
sudo: true
mode: "0600"Encryption: Argon2id key derivation + AES-256-GCM authenticated encryption. Each value gets a unique random salt and nonce, so encrypting the same plaintext twice produces different ciphertexts. The _verify sentinel lets stay-go detect a wrong password before touching anything.
stay-go [flags]
-c, --config string path to config directory (default "config")
--state string path to state file (default ~/.local/share/stay-go/state.json)
-d, --debug stream command output to the terminal
-n, --dry-run show plan without executing
-y, --yes auto-confirm without prompting
-S, --skipped show skipped items in the plan
--show [scope] print tracked state and exit (all, packages, users, …)
--version print version and exit
| Resource | What it manages |
|---|---|
packages |
System packages via the detected package manager |
files |
Files, symlinks, inline content, git repos, URLs |
users |
System users (shell, home, group membership) |
groups |
System groups |
services |
systemd system and user services |
scripts |
Shell scripts tracked by file content hash |
commands |
Inline shell commands tracked by content hash |
containers |
Docker / Podman containers |
distrobox |
Distrobox containers with in-box packages and commands |
flatpak |
Flatpak applications |
json |
Specific values inside existing JSON files |
secrets |
Age-encrypted secrets available to other resources |
MIT