Version-control your shell, editor, and tool configuration—with the same workflow for you and your coding agents.
dotctl helps you keep configuration files consistent across laptops, workstations, and remote development machines. Track the files you choose, review changes with Git, and pull them onto another computer without copying your entire home directory.
- Keep a history of your setup. Track files such as
.zshrc,.gitconfig, and editor configuration. - Bring your configuration to another machine. Clone your repository and back up files that would block checkout.
- Keep machine-specific settings. Select different files by operating system, architecture, hostname, or a named profile.
- Use it from an agent or script. Get structured JSON, preview operations, and run without interactive prompts.
- Store setup scripts alongside your workflow. Create and run local dependency scripts when you need them.
Under the hood, dotctl uses a bare Git repository: Git's history lives separately from the files, and your home directory is the work tree. Ordinary tracked files stay where your tools expect them; only machine-specific variants use symlinks. You can use any Git remote you can access, not just GitHub.
Dotctl operates on real configuration files. Review what you track and publish. It does not encrypt files or detect secrets—keep private keys, tokens, credentials, and machine-local state out of your repository.
Install · Quick start · Everyday examples · Machine-specific configuration · Agents and automation · Command reference
Requires Git. Prebuilt binaries support Linux and macOS, on amd64 and arm64; no Go installation is needed.
Download the installer into a temporary directory, inspect it, and run it:
installer_dir=$(mktemp -d)
curl --disable --proto '=https' --proto-redir '=https' -fsSL \
https://github.com/srctl/dotctl/releases/latest/download/install.sh \
-o "$installer_dir/install.sh"
less "$installer_dir/install.sh"
sh "$installer_dir/install.sh"
~/.local/bin/dotctl versionThe installer verifies the binary's SHA-256 checksum and installs into ~/.local/bin. It does not run sudo or edit your shell configuration. Add that directory to your PATH if needed:
export PATH="$HOME/.local/bin:$PATH"To keep that setting, add it to the appropriate startup file for your shell. To choose a different installation directory, run BIN_DIR=/path/to/bin sh "$installer_dir/install.sh". The destination must be trusted and writable; the installer refuses to replace symlink targets.
You can also download binaries and checksums.txt directly from Releases, or install with Go:
go install github.com/srctl/dotctl@latestWith go install, make sure your Go binary directory (usually ~/go/bin) is on PATH.
Non-interactive SSH may not read the same startup files as your terminal. If ssh my-server 'command -v dotctl' cannot find the binary, use its absolute path:
ssh my-server '~/.local/bin/dotctl version'
ssh my-server '~/.local/bin/dotctl pull'Alternatively, install into a writable directory already on the remote SSH PATH. Updating the binary alone does not change PATH.
Choose one of the following: start a new dotfiles repository, or use one you already have. The examples use home-relative paths, so run them from your home directory.
Git commits need a configured name and email. Use your existing Git identity, or set one for this repository after init with dotctl git config user.name "Your Name" and dotctl git config user.email "you@example.com".
Track an existing shell configuration file (substitute .bashrc or another file if you do not use Zsh):
cd "$HOME"
dotctl init
dotctl --dry-run track .zshrc --message "Track shell configuration"
dotctl track .zshrc --message "Track shell configuration"
dotctl statustrack adds and commits the file; it does not upload it anywhere. To sync across computers, first create an empty repository on your Git hosting service, then connect and push to it. Replace the URL below with your own:
dotctl add-remote https://github.com/YOUR_USERNAME/dotfiles.git
dotctl git push --set-upstream origin HEADA private repository is a useful default, but it is not a substitute for keeping secrets out of Git.
Your repository should contain paths relative to the home directory—for example, .zshrc at its root. Dotctl does not automatically convert repositories that depend on another tool's directory layout or templates.
cd "$HOME"
dotctl init --clone https://github.com/YOUR_USERNAME/dotfiles.git
dotctl --dry-run checkout --backup-existing
dotctl checkout --backup-existing
dotctl doctorcheckout --backup-existing moves only paths that would block the first checkout into a backup directory and reports its location. It leaves unrelated files alone. If no files need backing up, plain dotctl checkout is enough. Use --backup-dir /safe/new/path to choose a new backup destination.
| Data | Default location |
|---|---|
| Managed files (work tree) | Your home directory |
| Git history | ~/.cfg/.dotfiles |
| Dotctl configuration | ~/.config/dotctl/config |
| Local setup scripts | ~/.config/dotctl/runnables/ |
| Checkout and pull backups | ~/.config/dotctl/backups/ |
Configuration respects XDG_CONFIG_HOME. Use dotctl config show to inspect your actual paths. For an isolated environment, see Try it in a sandbox.
Once your repository has an upstream, the daily workflow is pull → edit → review → save → push:
cd "$HOME"
dotctl pull
# Edit your tracked configuration files in your preferred editor.
dotctl git diff
dotctl save --message "Update shell aliases and editor settings"
dotctl pushOn another computer, dotctl pull brings those committed changes into its work tree. Tools may need a reload or restart to pick them up.
To add an existing editor configuration file for the first time:
dotctl track .config/nvim/init.lua --message "Track Neovim configuration"save stages modifications and deletions to all already-tracked files. Use track for new files. Both commands also commit anything already staged, so review dotctl status and dotctl git diff --cached before committing unrelated work. commit is available when you want to commit only changes you have staged yourself.
Dotctl exposes Git directly, so there is no separate history format to learn:
dotctl list # Files managed by dotctl
dotctl is-tracked .zshrc # Check a specific file
dotctl status # Local changes and cached upstream status
dotctl git diff -- .zshrc # Review an unstaged change
dotctl git log --oneline -5 # Recent commitsInspection commands do not fetch in the background. Ahead/behind counts reflect the last fetched remote state.
A new development machine may already contain local edits or files that conflict with your repository. When you explicitly want the upstream version without losing the local content, use:
dotctl --dry-run pull --backup-existing
dotctl pull --backup-existing
dotctl statusThis mode:
- Preserves tracked staged and unstaged edits in a named stash, also anchored by a durable Git ref.
- Moves only untracked or ignored paths blocking incoming files into a private backup directory—not every untracked file in your home directory.
- Fast-forwards the branch and applies matching machine-specific variants.
The stash is not automatically restored or dropped. Dotctl prints recovery instructions and backup locations, including if a later step fails. Inspect saved edits with dotctl git stash show -p <stash_oid>; deliberately restore them with dotctl git stash apply --index <stash_oid>. Restoration against newer files may conflict. Compare backed-up files before copying them back.
Plain pull does not opt into this backup behavior. All pulls are fast-forward-only: divergent history and in-progress Git operations require manual resolution. A dry run never fetches, so its preview uses cached refs and may change after the real fetch. Pull backups default to CONFIG_DIR/backups/pull-TIMESTAMP; use --backup-dir to choose another new location.
Dependency scripts, called runnables, are useful for installing command-line tools or preparing an editor environment:
dotctl dependencies new tools # Create, stage, and open a scriptThis uses $EDITOR (default: nvim). Set it to your preferred editor, or pass --no-edit and edit the file yourself.
For example, on a Mac with Homebrew already installed, the script could contain:
#!/bin/sh
set -eu
brew install ripgrep fdAfter reviewing the script:
dotctl dependencies list
dotctl --dry-run dependencies run tools
dotctl dependencies run toolsdependencies new stages the new script in Git; use dotctl save after editing to commit its final contents, and dotctl push when you want to share it. Use dotctl config show to locate the runnable directory—it must be inside the managed work tree for script creation and staging to succeed.
Scripts run with your user permissions and can change your system. A dry run describes which script would run; it does not simulate its contents. Do not run scripts from an unfamiliar repository without inspecting them.
Keep different settings in one repository without maintaining a separate branch for each computer. A file name containing ## specifies the conditions under which it should be used:
| Example file | Selected on |
|---|---|
.gitconfig##profile.work |
Computers with the work profile |
.gitconfig##profile.personal |
Computers with the personal profile |
.zshrc##os.linux |
Linux computers |
.zshrc##os.darwin,arch.arm64 |
Apple Silicon Macs |
.gitconfig##hostname.workstation |
The host named workstation |
Before applying variants: if the plain path (such as ~/.gitconfig) already contains a real file, back it up and move it aside. If that plain file is tracked, it must also be untracked before becoming a variant link. Dotctl refuses to replace it automatically.
For example, create work and personal Git configuration variants with their respective names and email addresses, then track them:
cd "$HOME"
dotctl track '.gitconfig##profile.work' --message "Track work Git settings"
dotctl track '.gitconfig##profile.personal' --message "Track personal Git settings"
dotctl profile set work
dotctl profile show
dotctl profile list
dotctl --dry-run profile apply
dotctl profile applyprofile set chooses a name for this machine; profile apply creates the links. In this example, ~/.gitconfig becomes a symlink to .gitconfig##profile.work. Editing the linked file edits that variant, and dotctl save commits the change normally.
If application reports a conflict, resolve it before retrying. --force replaces conflicting local content without a backup and cannot override the tracked-path restriction; it is not a routine conflict-resolution step.
Every condition must match. The most specific variant wins: more conditions take precedence, followed by hostname, profile, arch, and os; equal matches are resolved by file name. Hostname matching uses the name before the first dot. Invalid selectors are reported.
checkout and pull apply matching variants automatically. Pass --skip-profile to either command to skip that step. If a pull succeeds but linking fails, dotctl reports PULL_PROFILE_FAILED; resolve the conflict and run dotctl profile apply.
Dotctl is a CLI, not an agent runner. Any coding agent with shell access can use the same commands you use, with JSON output, non-interactive execution, and dry-run plans. No model-specific integration is required.
Dotctl embeds an Agent Skills compatible skill covering inspection, safe changes, profiles, and recovery:
dotctl agent print-skill
dotctl --dry-run agent install-skill
dotctl agent install-skillThe default destination is ~/.agents/skills/dotctl/SKILL.md. Agents supporting that discovery location can load it after a reload or restart. If your agent expects skills elsewhere, use dotctl agent install-skill --path /path/to/skills/dotctl, or give it the output of print-skill. Use --force only when you intend to replace an existing skill.
Example requests to give an agent:
“Use dotctl to list my tracked shell and editor configuration. Summarize local changes without editing, fetching, or committing anything.”
“Track my existing
~/.config/starship.toml. Show me the plan, then commit it with the message ‘Track prompt configuration’. Do not push or include unrelated changes.”
“Check my dotfiles on this machine and pull the latest changes. If local files would conflict, explain the options and ask before setting any edits aside.”
For the Starship request above, an agent can first inspect local state:
cd "$HOME"
dotctl version --json
dotctl doctor --json
dotctl status --json
dotctl is-tracked .config/starship.toml --json
dotctl --json git diff --cacheddoctor is useful on a new machine or after a configuration problem; it is not required before every read-only query. If version or JSON commands are unsupported, upgrade the old binary before following this workflow.
After checking the file exists, reviewing its contents for secrets, and ensuring no unrelated changes are staged, preview the requested commit:
dotctl --json --dry-run track .config/starship.toml \
--message "Track prompt configuration"If the plan matches the user's approved request, execute it and verify:
dotctl --json track .config/starship.toml --message "Track prompt configuration"
dotctl status --json
dotctl --json git show --stat --oneline HEADThere is deliberately no push in this example. Agents should not publish changes, set local edits aside with pull --backup-existing, force profile replacement, or execute setup scripts unless the user authorized those effects.
--jsonemits one document on stdout and automatically disables prompts and editors.--non-interactiveprovides the same prompt-free behavior with human-readable output.- Supply
--messagefor unattendedtrack,save, andcommitoperations. --dry-rundescribes a mutation without applying it. Review the plan before running the real command.--yesconfirms supported operations, such asdependencies all; it is not permission to expand the user's request.- Put global flags before Git passthrough:
dotctl --json git status --short.
Each JSON response has a kind of result, plan, or error. For example, asking about an untracked file is a successful query:
{"ok":true,"kind":"result","data":{"path":".config/starship.toml","tracked":false}}Errors return a nonzero exit status and keep stderr empty:
{"ok":false,"kind":"error","error":{"code":"CONFIG_NOT_FOUND","message":"dotctl config not found"}}Git and runnable output is captured in data.output. Pull results and errors can also include stash_oid, stash_ref, backup_dir, backed_up, and recovery; agents should retain and report those recovery details, not treat a failed command as proof that nothing changed.
JSON error-code reference
General codes: CONFIG_NOT_FOUND, CONFIG_INVALID, INVALID_ARGUMENT, PERMISSION_DENIED, EXTERNAL_COMMAND_FAILED, DOCTOR_UNHEALTHY, JSON_UNSUPPORTED, and the fallback COMMAND_FAILED.
Profile conflicts use PROFILE_CONFLICT, with details in data.conflicts. Binary update failures use UPDATE_FAILED.
Pull codes: PULL_UNSAFE, PULL_UPSTREAM, PULL_FETCH_FAILED, PULL_DIVERGED, PULL_BLOCKED, PULL_BACKUP_FAILED, PULL_STASH_FAILED, PULL_PROFILE_FAILED, and PULL_FAILED.
For testing or agent experimentation, override both the configuration directory and the work tree. This example creates an isolated environment without managing your actual home directory; the subshell keeps its overrides out of your normal session:
(
sandbox=$(mktemp -d)
export DOTCTL_CONFIG_DIR="$sandbox/config"
export DOTCTL_WORK_TREE="$sandbox/home"
mkdir -p "$DOTCTL_WORK_TREE"
dotctl --json --dry-run init
dotctl --json init
dotctl doctor --json
printf 'Sandbox kept at: %s\n' "$sandbox"
)The equivalent flags are --config-dir and --work-tree. Do not supply overrides when the intention is to manage the user's real dotfiles.
| Command | Purpose |
|---|---|
init / init --clone URL |
Start a new repository or clone an existing one |
checkout --backup-existing |
Populate files safely on the first checkout |
status, list, is-tracked PATH |
Inspect managed files |
track PATH -m "message" |
Add and commit a file |
save -m "message" |
Stage and commit changes to tracked files |
commit -m "message" |
Commit already-staged changes |
pull / push |
Receive or publish dotfile commits |
profile show, profile list, profile apply |
Inspect and apply machine-specific variants |
dependencies list / dependencies run NAME |
Inspect or run local setup scripts |
doctor / config show |
Check local setup and inspect paths |
git ARGS... |
Run Git against the dotfiles repository |
agent install-skill |
Install instructions for compatible agents |
version / update |
Inspect or update the dotctl binary |
Run dotctl --help or dotctl COMMAND --help for flags and additional commands. Network access occurs only for explicit operations such as cloning, pulling, pushing, binary update checks, or commands/scripts you choose to run.
dotctl version # Also: dotctl --version
dotctl update --check # Query the latest stable release
dotctl --dry-run update # Preview without network access
dotctl update # Install the latest stable binaryupdate changes the binary, not your dotfiles. It follows executable symlinks, verifies SHA-256 before atomic replacement, and preserves the existing binary on installation failure. It does not downgrade a newer stable release; development builds are replaced with the latest stable version. Checksums verify download integrity, not an independently signed release signature.
These commands work before init and support --json. The executable's directory must be writable; use your package manager instead for package-managed installations. Older binaries without self-update need the installer once.
Upgrading from a pre-v0.1.0 build? The old dotfile-saving update command is now save. Update shell aliases and scripts. Old update --message / update --patch calls fail with migration guidance; bare update now updates the binary. See the changelog.
Building from source requires Go 1.21 or newer. The full checks also require Python 3; Task is optional.
task install # Build and install into ~/.local/bin
task check # Formatting, vet, Go tests/build, and script testsWithout Task:
gofmt -w .
go vet ./...
go test ./...
go build ./...
python3 -B -m unittest discover -s scripts/tests -p 'test_*.py' -vTo build release artifacts locally without publishing:
bash scripts/release-build.sh v0.1.0Artifacts go into dist/, with the version embedded in the binaries. Maintainers publish by pushing a stable vMAJOR.MINOR.PATCH tag. The release workflow checks Linux and macOS before building all four binaries and publishing a complete release with checksums. A failed upload remains a draft rather than becoming a partially downloadable latest release.