Reliable dotfile and configuration deployment for local machines and SSH targets.
clink describes every destination in one config.yaml, turns that file into an execution plan, previews the writes, captures backups, and then deploys through symlinks, local copies, or SSH uploads. It is built for people who want reproducible dotfiles and small configuration rollouts without giving up inspection and restore.
Describe every target once. Preview every write. Restore when needed.
flowchart LR
config["config.yaml"] --> plan["Plan"]
plan --> backup["Backup current targets"]
backup --> deploy{"Deploy"}
deploy --> symlink["symlink"]
deploy --> copy["copy"]
deploy --> ssh["ssh upload"]
symlink --> check["check"]
copy --> check
ssh --> check
backup --> restore["restore"]
- One inventory for local and remote targets: manage dotfiles, app config, and server files from the same YAML.
- Plan before writing: use dry-run output to inspect exactly what
clinkis going to do. - Backups by default: every apply run records previous target state under
~/.clink/<timestamp>/. - Drift checks: verify symlink targets, copied file hashes, and SSH target presence before or after deployment.
- Local web console: inspect status, edit config, preview writes, diff local files, and restore backups from
127.0.0.1.
curl -fsSL https://raw.githubusercontent.com/alexmaze/clink/main/scripts/install.sh | shInstall a specific release:
curl -fsSL https://raw.githubusercontent.com/alexmaze/clink/main/scripts/install.sh | sh -s -- v1.2.1By default, the installer writes to ~/.local/bin. To choose another directory:
curl -fsSL https://raw.githubusercontent.com/alexmaze/clink/main/scripts/install.sh | CLINK_INSTALL_DIR=/usr/local/bin shOther options:
go install github.com/alexmaze/clink@latestmake buildCreate a config.yaml:
mode: symlink
rules:
- name: shell
items:
- src: ./dotfiles/.zshrc
dest: ~/.zshrcPreview the plan:
clink apply -c ./config.yaml --dry-runApply it:
clink apply -c ./config.yamlValidate the current state later:
clink check -c ./config.yamlRestore from the latest backup in non-interactive mode:
clink restore --non-interactiveconfig.yaml can mix local symlinks, local copies, and SSH uploads:
mode: symlink
hooks:
pre: echo "start apply"
post: echo "finish apply"
vars:
APP_HOME: /opt/myapp
ssh_servers:
prod:
host: 192.168.1.10
port: 22
user: root
key: ~/.ssh/id_rsa
rules:
- name: shell
items:
- src: ./dotfiles/.zshrc
dest: ~/.zshrc
- name: app-config
mode: copy
items:
- src: ./app/config.yaml
dest: ${APP_HOME}/config.yaml
- name: remote-nginx
mode: ssh
ssh: prod
items:
- src: ./nginx/nginx.conf
dest: /etc/nginx/nginx.confConfiguration rules:
- Top-level
modeis the default distribution mode. If omitted, it defaults tosymlink. rules[].modecan override the top-level default for one rule.- Local
srcanddestsupport relative paths, absolute paths, and~/. - SSH mode requires an absolute remote
dest. - Variables use
${VAR_NAME}syntax. - If an SSH server has neither
keynorpassword, the CLI prompts for a password at runtime.
| Command | Purpose |
|---|---|
clink apply -c <config.yaml> |
Build a plan, back up existing targets, and deploy configured items. |
clink check -c <config.yaml> |
Validate current target state against the config. |
clink restore --backup <id-or-path> |
Restore targets from a backup manifest. |
clink add -c <config.yaml> <source> |
Bring an existing local file under clink management. |
clink web -c <config.yaml> |
Open the local browser console for status, config, and backups. |
clink version |
Print the current version. |
Common flags:
-c, --config: specify the config file path.-r, --rule: filter rules by name or 1-based index; can be repeated.-d, --dry-run: generate the plan without executing it.-y, --yes: skip confirmation.--non-interactive: disable interactive input.--output text|json: choose text or JSON output.
apply always plans from the config before execution. During execution, it captures the previous destination state first:
- Local modes back up the original target to
~/.clink/<timestamp>/payload/.... - SSH mode downloads the original remote file into the same backup directory first.
- Every run writes
config.snapshot.yaml,manifest.json, andpayload/.
Example backup layout:
~/.clink/
20260406_120000/
config.snapshot.yaml
manifest.json
payload/
shell/
home/alex/.zshrc
remote-nginx/
etc/nginx/nginx.conf
manifest.json records the rule name, mode, source, destination, backup file path, SSH server, path kind, and backup content hash for each backed-up target.
Restore behavior:
clink restoreopens an interactive backup picker when--backupis omitted.clink restore --non-interactiveselects the latest backup when--backupis omitted.- Local restore writes through a temporary path and then atomically replaces the target.
- SSH restore reuses connections grouped by server.
- Backups from older versions are not supported; a backup without
manifest.jsoncannot be restored.
Run:
clink web -c ./config.yamlThe console listens on 127.0.0.1:19877 by default and uses the same config.yaml as the CLI. It can:
- show status for
symlink,copy, andsshrules; - preview and execute linking for all items or a single item;
- show local diffs for
symlinkandcopyitems; - list backups, preview restore contents, and execute restore;
- edit config through a structured form or raw YAML with validation before saving.
SSH operations in the web UI require key or password in ssh_servers; unlike the CLI, the web UI does not prompt for missing SSH passwords.
Security note: the web UI can modify the config file and write to configured destinations. Keep the default localhost binding unless you explicitly trust the network where it is exposed.
make build
make test
make build-all
make releasemake buildcreates the local./clinkbinary and injects version metadata.make testrunsgo test github.com/alexmaze/clink/....make build-allbuilds Linux and macOS release artifacts intodist/.make releaseasks for a release version, updates release metadata, commits it, then creates and pushes av*tag to trigger the GitHub Actions release workflow../run_tests.shruns the full test suite in Docker or Podman and prints a coverage summary.
This version has been fully refactored with these constraints:
- Linux and macOS only.
- Unified subcommand-based CLI.
- Runtime output and interaction are in English.
- Legacy command entrypoints are not supported.
- Old backup directory layouts are not supported.
When upgrading from an older version, generate new backups and use the new command structure.