store is a dotfile manager for Linux, macOS, and Windows. You keep your config files in one git repo; store reads a single YAML file and creates the symlinks that place each file where the relevant program expects to find it — ~/.zshrc, ~/.config/nvim, and so on. Unlike GNU Stow, your repo directory structure does not have to mirror your home directory — target paths are declared explicitly. One repo, one config file, one command per machine.
Bare
storeis non-destructive — it prints help.store applyis the verb that reconciles symlinks.store tuiopens an interactive dashboard.
Upgrading from 1.x? See MIGRATING.md — the only breaking change is that bare
storenow prints help instead of reconciling; runstore applyto reconcile.
stock installs the packages, tools, and runtimes your dotfiles depend on. Same .store/ directory, same when: platform filters, same hook env contract — declared in .store/packages.yaml instead of config.yaml. Pair metaphor: you stock a store with inventory.
Install stock via the same channels store uses (AUR, Nix, go install, prebuilt binaries) and then invoke it as either a standalone binary or through store's git-style dispatch:
stock doctor # direct
store stock doctor # delegated by store if `stock` is on $PATHstore's TUI header picks up a dim stock signpost when the binary is available.
- Overview
- How It Compares
- Installation
- Quick Start
- Concepts
- Config Format
- Hooks
- Templates & Secrets
- Platform Conditionals
- Ignoring Files
- Internals
- Interactive TUI
- Commands
- Shell Completion
- Status Indicators
- Troubleshooting
Each store is a top-level directory in your repo. The config in .store/config.yaml tells store where that directory, or selected files inside it, should appear on disk.
Example repo layout:
~/dotfiles/
.store/config.yaml
nvim/
init.lua
lua/
shells/
.zshrc
.bashrc
config.fish
config.nu
git/
.gitconfig
Example result after running store apply:
~/.config/nvim -> ~/dotfiles/nvim
~/.zshrc -> ~/dotfiles/shells/.zshrc
~/.bashrc -> ~/dotfiles/shells/.bashrc
~/.config/fish/config.fish -> ~/dotfiles/shells/config.fish
~/.config/nushell/config.nu -> ~/dotfiles/shells/config.nu
~/.config/git -> ~/dotfiles/git
| Differentiator | GNU Stow | store |
|---|---|---|
| Directory structure | Must mirror the target filesystem hierarchy | Uses top-level store directories plus explicit config |
| Target paths | Inferred from package layout and invocation directory | Declared explicitly per store or target in YAML |
| Configuration | Convention-based | Single .store/config.yaml file |
| Granularity | Primarily package/file symlinks based on layout | Whole-directory mode or file-mode with files/patterns |
| Multiple targets | Usually split across separate packages | One store can deploy to multiple target paths |
| Conflict handling | Stops and requires manual cleanup | Detects conflicts and can move existing files into the store |
| Setup on new machine | Run stow from the right place with the right packages |
Run store apply anywhere inside the repo |
| Secret management | No built-in encrypted secret store | Encrypted secrets plus {{ secret "name" }} templates |
| Platform conditionals | No built-in platform filters | when: supports OS, arch, distro, hostname, shell, and WSL |
| Ignore patterns | Manual exclusions or separate packages | Built-in global ignores plus per-store/per-target ignore: |
| Shell completion | Shell-specific setup outside the tool | store completion generates scripts for major shells |
| Health checks (doctor) | No built-in diagnostics command | store doctor checks config, targets, secrets, and platform skips |
| Import existing symlinks | No import command | store import scans existing symlinks and writes config |
| Adopt existing files | Manual move-and-link workflow | store adopt moves files into the repo and symlinks back |
| Dry run preview | No built-in preview command | store diff shows create/replace/conflict actions before changes |
| Interactive UI | None | store tui — dashboard with a command palette |
- chezmoi. Chezmoi renders every file through its template engine by default and keeps a shadow source directory;
storerenders only files that reference templates and never shadows — it creates plain symlinks from the target to your repo. If you wantgit diffin the repo to reflect exactly what's on disk,storeis simpler. If you need deep per-host state that isn't committed, chezmoi handles that out of the box. - dotbot. Dotbot reads an install YAML and creates links.
storedoes too, with a few additions: explicit conflict handling, anadoptcommand that moves existing files into the repo, encrypted secrets, platform conditionals, and a healthdoctorpass. Existing dotbot users can usually get most of the way withstore import. - yadm. Yadm wraps git and treats
~as the working tree.storekeeps the repo separate and links into~. Choose yadm if you wantgit statusover your home directory; choosestoreif you want a repo you cancdinto without~surprises.
# Precompiled binary
$ yay -S store-bin
# Build from source
$ yay -S store
# Track latest development
$ yay -S store-git# run once
$ nix run github:cushycush/store -- --help
# install into your profile
$ nix profile install github:cushycush/storeThe flake also exposes a dev shell with Go and gopls pinned to the versions CI uses:
$ nix develop github:cushycush/storeDownload the archive for your OS and architecture from the releases page and drop store (or store.exe) onto your PATH.
$ go install github.com/cushycush/store/v2/cmd/store@latest$ git clone https://github.com/cushycush/store.git
$ cd store
$ make build
$ mv store /usr/local/bin/Windows: creating symlinks requires either Developer Mode enabled in Settings → Privacy & Security → For developers, or running your terminal as Administrator.
store doctordetects the missing capability and warns on first run. Alternatively, runstorefrom WSL.
- Initialize a dotfiles repo:
$ mkdir ~/dotfiles
$ cd ~/dotfiles
$ git init
$ store init- Adopt an existing config directory (moves it into the repo and symlinks back):
$ store adopt ~/.config/nvimOr create a store manually and point it at a target:
$ store add nvim ~/.config/nvim- Add a file-level store for files that live in your home directory:
$ store add shells ~ -f .zshrc -f .bashrc- Add more targets to the same store:
$ store target add shells -t ~/.config/fish -f config.fish
$ store target add shells -t ~/.config/nushell -f config.nu- Commit the repo:
$ git add -A
$ git commit -m "add dotfiles"
$ git push- Restore everything on a new machine:
$ git clone https://github.com/you/dotfiles.git ~/dotfiles
$ cd ~/dotfiles
$ store applyIf target files already exist, store apply detects the conflict, offers to move those files into the repo, and then creates the symlinks.
Windows example. Adopt a VS Code settings directory and adopt a PowerShell profile file:
> store adopt $env:APPDATA\Code\User
> store adopt $HOME\Documents\PowerShell\Microsoft.PowerShell_profile.ps1Optional. Run store tui for an interactive dashboard — every CLI verb is accessible from inside it. See Interactive TUI.
Six ideas to keep in mind before you read the command reference. The rest of the README — Config Format, Hooks, Secrets, Commands — assumes these.
-
Store. A top-level directory in your repo (
nvim/,shells/,git/). Each store is independently configured in.store/config.yamland can be linked or removed on its own. -
Target. The path on disk where a store (or selected files from it) gets symlinked:
~/.config/nvim,~/.zshrc, and so on. Targets are declared explicitly —storenever guesses based on directory names. -
Whole-directory mode vs file mode. A store either links its directory as a single symlink or links selected files into a target directory.
- No
filesand nopatterns→ whole-directory mode. Example:nvim/is linked as~/.config/nvim. - Any
filesorpatterns→ file mode. Example:.zshrcand.bashrcfromshells/land individually inside~alongside non-dotfile content. filesandpatternscan be used together; matches are combined and deduplicated.ignoreapplies in both modes.- If whole-directory mode would include ignored content,
storeautomatically promotes that target to file mode so ignored files never appear at the target.
- No
-
Single-target vs multi-target. Use
target:when a store goes to one place. Usetargets:when one store's files fan out to several locations — for example,shellswith.zshrclanding in~,config.fishin~/.config/fish, andconfig.nuin~/.config/nushell. You can switch between the two forms at any time;store target addandstore target removemigrate automatically. -
The config is the source of truth.
.store/config.yamlfully describes the symlink state you want on a machine. Runningstore applyreconciles the filesystem to match — it creates missing links, replaces broken ones, and leaves correct ones alone. Changing the config and runningstore applyagain is the entire update loop. -
Conflict handling. If a target path already exists as a real file or directory (not a
store-managed symlink),storestops before linking and offers to move the existing content into the repo and then symlink back. Nothing gets overwritten silently.
Configuration lives in .store/config.yaml.
Top-level format:
vars:
editor: nvim
stores:
nvim:
target: "~/.config/nvim"
shells:
target: "~"
files:
- .zshrc
- .bashrc
ignore:
- "*.bak"
hooks:
post: "printf 'shells linked\n'"
when:
os: linuxMulti-target format:
stores:
shells:
targets:
- target: "~"
files:
- .zshrc
- .bashrc
- target: "~/.config/fish"
files:
- config.fish
- target: "~/.config/nushell"
patterns:
- "*.nu"
ignore:
- "*.bak"Nested groups:
Once you have more than a handful of stores, slash-named keys like
desktop/hyprland start to feel cluttered. You can group them under a
plain YAML map instead, and the resulting store names are derived from
the keys joined with /:
stores:
desktop:
hyprland:
target: "~/.config/hypr"
waybar:
target: "~/.config/waybar"
editors:
neovim:
target: "~/.config/nvim"
kmonad:
target: "~/.config/kmonad"That config defines four stores: desktop/hyprland, desktop/waybar,
editors/neovim, and kmonad. The store directories on disk still live
at the same paths as the slash-joined names. Both forms read the same way
internally, so you can freely mix them, and any store you add or modify
through the CLI or TUI is saved back in the nested form when there is no
ambiguity.
Rules:
- A store may use either
targetortargets, not both. - In multi-target mode,
files,patterns, andignorebelong inside eachtargets[]entry. - If a store entry has no target yet, it is valid config but nothing is linked.
varsis a top-level map alongsidestores; values are accessible in templates as{{ .Vars.key }}.- A YAML map is treated as a store entry when it has any of
target,targets,files,patterns,ignore,hooks, orwhen. Otherwise it is treated as a group containing more stores. - A store name and a group cannot share a path. If you have both
shells(a store) andshells/fish(another store), the file falls back to flat slash keys for those entries so each one stays addressable.
| Field | Required | Description |
|---|---|---|
target |
No | Target path for a single-target store |
files |
No | Explicit list of files to link individually |
patterns |
No | Glob patterns to match files; supports ** |
ignore |
No | Glob patterns to exclude from linking |
hooks |
No | Per-store pre and post shell commands |
when |
No | Platform filter for this store |
targets |
No | List of per-target entries for multi-target mode |
| Field | Required | Description |
|---|---|---|
targets[].target |
Yes | Target path for this target entry |
targets[].files |
No | Explicit files to link individually for this target |
targets[].patterns |
No | Glob patterns to match files for this target |
targets[].ignore |
No | Glob patterns to exclude for this target |
targets[].when |
No | Platform filter scoped to this target only |
| Field | Required | Description |
|---|---|---|
hooks.pre |
No | Command run before link or unlink work for the store |
hooks.post |
No | Command run after link or unlink work for the store |
| Field | Required | Description |
|---|---|---|
when.os |
No | OS name such as linux, darwin, or windows |
when.arch |
No | CPU architecture such as amd64 or arm64 |
when.distro |
No | Distro or OS family such as ubuntu, arch, macos, or windows |
when.distro_version |
No | Distro version string |
when.hostname |
No | Hostname to match |
when.shell |
No | Current shell name such as zsh, bash, fish, or nu |
when.wsl |
No | Whether the machine is running under WSL |
| Pattern | Matches |
|---|---|
.zsh* |
.zshrc, .zshenv, and similar top-level files |
*.conf |
Top-level .conf files |
**/*.conf |
.conf files at any depth |
config/*.lua |
.lua files directly inside config/ |
**/*.{lua,vim} |
.lua and .vim files recursively |
~and~/...are supported on the CLI and in config.- In YAML, every
~path must be quoted, for exampletarget: "~"ortarget: "~/.config/nvim". - Relative CLI target paths are resolved to absolute paths before being saved.
Hooks let you run shell commands around store operations. Use them for post-link reloads, cache rebuilds, or validation steps tied to a specific store or to the whole repo.
Per-store hook example:
stores:
tmux:
target: "~/.config/tmux"
hooks:
post: "tmux source-file ~/.config/tmux/tmux.conf 2>/dev/null || true"
nvim:
target: "~/.config/nvim"
hooks:
pre: "nvim --headless -c 'checkhealth' -c 'qa' 2>/dev/null"Global hook layout:
.store/hooks/
pre-store
post-store
pre-remove
post-remove
How it works:
- Per-store hooks are configured in YAML and executed with
sh -con Linux and macOS, and withcmd.exe /Con Windows. - Global hooks live in
.store/hooks/. On Linux and macOS they must have the executable bit set and are run directly via their shebang. - On Windows the executable bit is ignored; global hooks are dispatched by file extension:
.ps1scripts run under PowerShell,.cmd/.bat/.exerun directly, and anything else is attempted viash(works under Git Bash or WSL). prehooks abort that store on failure.posthooks only warn on failure.pre-storeandpre-removeabort the whole command on failure.- Hooks receive
STORE_ROOT,STORE_ACTION, and detected platform variables; per-store hooks also receiveSTORE_NAMEandSTORE_TARGET.
Files in a store can contain Go text/template expressions. When store links or restores a store, it renders these templates into a local staging directory and symlinks the rendered output. Files without template expressions are symlinked directly.
A file is treated as a template only when it references one of the names store knows about: the secret or env function, or one of the .Hostname / .OS / .Arch / .Distro / .Shell / .Vars data fields. Files that contain unrelated {{ ... }} content (GitHub Actions expressions, Helm charts, Jinja or Handlebars examples in docs, etc.) are passed through verbatim.
| Expression | Description |
|---|---|
{{ secret "name" }} |
Looks up an encrypted secret by name |
{{ env "VAR" }} |
Reads an environment variable |
{{ .Hostname }} |
Current machine hostname |
{{ .OS }} |
Operating system (linux, darwin, windows) |
{{ .Arch }} |
CPU architecture (amd64, arm64, etc.) |
{{ .Distro }} |
Distro or OS family (ubuntu, arch, macos, etc.) |
{{ .Shell }} |
Current shell name (zsh, bash, fish, etc.) |
{{ .Vars.key }} |
User-defined variable from config vars: map |
The top-level vars: map in .store/config.yaml defines key-value pairs accessible as {{ .Vars.key }} in templates:
vars:
editor: nvim
email: user@example.com
stores:
git:
target: "~/.config/git"[user]
email = {{ .Vars.email }}
[core]
editor = {{ .Vars.editor }}
A secret is an encrypted value stored in .store/secrets.enc and rendered using {{ secret "name" }}.
[github]
token = {{ secret "github_token" }}
$ store secret set github_token
$ store applyHow it works:
storescans the selected store directories and only prompts for a passphrase if rendering is needed.- Secrets are stored in
.store/secrets.enc. - Rendered files are written into a local staging directory under
$XDG_STATE_HOME/store/<repo-hash>/or~/.local/state/store/<repo-hash>/ifXDG_STATE_HOMEis unset. - Files without template expressions are symlinked directly; rendered files are symlinked from the staging directory.
- Encryption uses Argon2id for key derivation and XChaCha20-Poly1305 for authenticated encryption.
- If a referenced secret is missing, the render step fails and the store operation stops.
- The passphrase comes from
STORE_PASSPHRASEor hidden interactive input. .store/secrets.encis designed to be committed; the plaintext secrets are not.
Platform conditionals let one repo serve multiple machines without separate branches or manual edits.
Example:
stores:
linux-shell:
target: "~"
files:
- .bashrc
when:
os: linux
work-tools:
target: "~/work"
when:
hostname: work-laptop
wsl-tools:
target: "~/.local/bin"
when:
wsl: trueHow it works:
storecompares each populatedwhen:field against detected platform info.- All specified fields must match.
- Stores without
when:always apply. - Detected values include OS, arch, distro, distro version, hostname, shell, and WSL state.
- Commands that operate on the configured set, such as
store apply,store diff, andstore status, skip non-matching stores. store doctorreports skipped stores as informational issues so you can confirm the filter is doing what you expect.
The same when: clause is also valid inside a targets[] entry, which is the right tool when one repo directory routes to different paths per platform. Helix is the canonical example: its config lives under ~/.config/helix on Linux and ~/AppData/Roaming/helix on Windows, and you want exactly one symlink on each machine.
stores:
helix:
targets:
- target: "~/.config/helix"
when:
os: linux
- target: "~/AppData/Roaming/helix"
when:
os: windowsA target without a when: always applies; a non-matching one is skipped on link, unlink, and status. store doctor reports skipped targets the same way it reports skipped stores. Per-target and store-level filters compose: if the store-level when: excludes the platform, none of its targets run.
Ignore patterns exclude files or directories from linking. This is useful for editor backups, nested metadata, scratch files, or partial stores that should not be exposed at the target.
Example:
stores:
nvim:
target: "~/.config/nvim"
ignore:
- "*.bak"
- "scratch/"
- "**/*.test.lua"How it works:
- Global ignore patterns are always active:
.store,.store/**,.git,.git/**,.gitignore,.DS_Store. - User-defined
ignore:patterns are added on top of the global ignore set. - Directory ignore patterns such as
scratch/exclude the whole directory tree. - Ignore matching uses the same glob engine as
patterns:. - If a target has
ignore:or if the store contains globally ignored files, whole-directory mode is automatically promoted to file mode so ignored content never appears at the target. ignore:may be defined at the top level for single-target stores or inside eachtargets[]entry for multi-target stores.
Implementation details a user might bump into at the edges. You don't need this to use store; it's here so the edges are documented.
- Root discovery. Commands can run from any subdirectory inside the repo;
storewalks upward until it finds.store/. - Portable target storage. CLI target paths keep
~prefixes when possible so config stays portable across machines. - Absolute symlinks. Link targets are resolved to absolute source paths so the working directory does not matter.
- Conflict resolution. Before linking,
storedetects files or directories already occupying the target and offers to move them into the repo. If files already exist in the store where moved content would land,storelists the.bakbackups it would create. - Whole-directory vs file mode. No
filesorpatternsmeans whole-directory mode unless ignored content forces promotion to file mode. - Template staging. Secret-backed templates are rendered into a machine-local staging directory and linked from there.
- Performance. Explicit files use direct stat checks, non-recursive globs avoid full walks, and only
**patterns trigger recursive traversal.
store tui opens a keyboard-driven dashboard. Every CLI verb is reachable from inside it. A handful run through single-letter bindings, the rest through the : command palette. The dashboard reads as a single vertical column: a ledger of your stores on top, the selected store's detail below, and the recent activity log beneath that. No panes, no outer frame.
store ~/dotfiles linux/amd64 ·
─── 8 stores · 6 linked 2 missing ─────────────────────────────────
▸ + desktop 4 stores ● linked
+ editors 1 store ● linked
− shells
fish ~/.config/fish ● linked
nushell ~/.config/nushell ○ missing
+ tools 1 store ● linked
kmonad ~/.config/kmonad ● linked
─── desktop/ ────────────────────────────── 4 stores linked ─
press l or enter to expand
─── recent ─────────────────────────────────────────────────────────
recent ✓ refreshed 2s
j/k move enter actions space toggle / filter : command ? help
When stores share a slash-prefix, the ledger groups them under a header
row so you can scan the top level first. A + next to the group means
collapsed; a − means open. Top-level stores (no slash in the name) sit
alongside groups at the same indent.
Navigate
| Key | Action |
|---|---|
j / k |
Move up / down the store list |
g / G |
Top / bottom |
l / → |
Expand a group, or open the action menu on a leaf store |
h / ← |
Collapse the current group, or jump from a child row to its parent |
esc |
Back. Clears an active filter and closes the top overlay |
Stores
| Key | Action |
|---|---|
enter |
Toggle a group's expand state, or open the action menu on a leaf store |
space |
Toggle a group's expand state, or link / unlink the selected store |
d |
Diff. Preview to the activity log |
A |
Apply all (reconcile every store) |
R |
Remove the selected store (typed confirmation required) |
/ |
Filter the store list live. Filter mode flattens matches across all groups |
Commands
| Key | Action |
|---|---|
: |
Open the command palette |
\ |
Toggle full-screen activity log |
r |
Refresh from disk |
? |
Show this keymap inside the TUI |
q / ctrl+c |
Quit |
Press : anywhere to open a fuzzy-match palette over every CLI verb: apply, init, import, adopt, add, modify, remove, remove --all, list, path, rename, edit, status, diff, doctor, version, target {add,remove,modify,when}, secret {set,get,remove,list}. Pick an entry with enter; the palette prompts inline for any required arguments.
target when is the editing path for per-target platform filters: pick a store and a target, then type a key=value expression like os=linux,darwin shell=zsh. Submit an empty value to clear the filter. The store is unlinked and re-linked in place so the symlink reflects the new filter on the current platform.
remove <name> and remove --all require typing the store's name (or the literal phrase remove all) to confirm. Single-store link/unlink is one keystroke; anything broader gates behind a typed confirmation. The safety floor rises with blast radius.
A multi-target store's detail section shows each target as its own collapsible sub-rule with a per-target link count (3/3 linked). space expands or collapses the focused target. Each target can be linked, unlinked, or modified individually through the action menu's target… entry.
The detail pane shows target, mode, platform, when: match status, configured hooks, and — when the store contains template files — a templates row summarising how many distinct secrets and vars keys are referenced. Counts are cached per store and refreshed on r.
Every operation writes an entry to the activity log. The main view shows the latest entry on a single line; press \ for a full-screen view with scroll (j/k). The log is a 200-entry ring buffer — older entries fall off as new ones arrive.
Reference for every subcommand. For a task-oriented tour, see Quick Start and Concepts. Run store --help for the live CLI tree.
Jump to: apply · init · import · adopt · add · modify · list · path · rename · edit · target add · target remove · target modify · remove · status · diff · doctor · tui · version · secret set · secret get · secret remove · secret list · completion
Reconciles all configured stores: creates missing symlinks, replaces broken ones, and reports conflicts. Run this after cloning a dotfiles repo on a new machine or after editing .store/config.yaml. Running store with no arguments prints help; store apply is the explicit verb that performs the reconciliation.
| Flag | Short | Description |
|---|---|---|
--only |
-o |
Apply only the named stores (repeatable) |
--dry-run |
Preview changes without applying (equivalent to diff) |
|
--force |
Create .bak backups without prompting |
$ store apply -o nvim -o git
$ store apply --dry-run
$ store apply --force$ store apply
Storing all stores:
nvim -> ~/.config/nvim
shells -> ~ (files)
shells -> ~/.config/fish (files)
shells -> ~/.config/nushell (files)
How it works:
- Filters out stores whose
when:clause does not match the current platform. - Runs global
pre-storeandpost-storehooks if present. - Detects conflicts before linking and prompts once for all selected stores.
- Prompts for a secrets passphrase only if one of the selected stores needs template rendering.
Creates .store/config.yaml in the current directory.
$ store initInitialized store config at .store/config.yaml
Use this once at the root of a new dotfiles repo.
Scans for existing symlinks that already point into the repo and imports them into .store/config.yaml.
| Flag | Description |
|---|---|
--scan-dir |
Directories to scan (repeatable). Defaults to ~, ~/.config, ~/.local/share, ~/.local/bin |
--dry-run |
Print what would be imported without writing config |
$ store import --dry-run
$ store import --scan-dir ~/.config --scan-dir ~/.local/bin$ store import --dry-run
Scanning for symlinks pointing into /home/user/dotfiles...
Found:
nvim ~/.config/nvim -> nvim/ (whole directory)
shells ~/.zshrc -> shells/.zshrc (file)
shells ~/.bashrc -> shells/.bashrc (file)
Dry run: would import 3 symlinks as 2 stores
How it works:
- Scans the immediate contents of each scan directory for symlinks.
- Keeps only links whose targets resolve inside the repo and point at a top-level store directory.
- Groups discovered links into single-target or multi-target store entries.
- Skips stores that already exist in config.
- Without
--dry-run,store importprints the discovered mapping and asks for confirmation before writing config. The persistent--forceflag skips that confirmation prompt.
Takes an existing file or directory, moves it into the repo, creates a config entry, and symlinks back to the original location.
| Flag | Short | Description |
|---|---|---|
--name |
-n |
Override the derived store name |
--dry-run |
Preview what would happen without making changes | |
--files |
-f |
Only adopt specific files from a directory; repeatable |
--patterns |
-p |
Only adopt files matching glob patterns; repeatable |
$ store adopt ~/.config/nvim
$ store adopt ~/.zshrc
$ store adopt ~/.config/nvim --name vim
$ store adopt ~/.config/app -f config.toml -f settings.json
$ store adopt ~/.config/nvim --dry-run$ store adopt ~/.config/nvim
Adopting:
~/.config/nvim -> nvim/ (directory)
Proceed? [y/N] y
nvim -> ~/.config/nvim
How it works:
- Directories are moved whole into the repo and symlinked back.
- Single files are placed into a store directory named after the file (leading dots stripped), with the target set to the file's parent directory.
- The store name is derived from the path basename. Use
--nameto override. --filesand--patternsadopt only matching files from a directory, creating a file-mode store entry.- Rejects paths that are already symlinks — use
store importfor those.
Creates a store directory if needed, adds the config entry, and immediately links it if a target is provided. The target path may be passed positionally or via -t/--target.
| Flag | Short | Description |
|---|---|---|
--target |
-t |
Target path (or pass positionally) |
--files |
-f |
Explicit files to link individually; repeatable |
--patterns |
-p |
Glob patterns to match files; repeatable; supports ** |
$ store add nvim ~/.config/nvim
$ store add shells ~ -f .zshrc -f .bashrc
$ store add configs ~/.config -p "**/*.conf"
$ store add nvim -t ~/.config/nvim # flag form still works
$ store add git # no target yet; linked laterHow it works:
- Without a target, the store is saved but not linked yet.
- Without
--filesor--patterns, the entire store directory is linked. - Relative targets are resolved to absolute paths before being saved.
- Passing both a positional target and
--targetis an error; pick one.
Updates an existing single-target store. Choose between wholesale replacement, incremental changes, or both.
| Flag | Short | Description |
|---|---|---|
--target |
-t |
Replace the target path |
--files |
-f |
Replace the file list; repeatable |
--patterns |
-p |
Replace the pattern list; repeatable |
--add-file |
Append a file to the file list; repeatable | |
--remove-file |
Remove a file from the file list; repeatable | |
--add-pattern |
Append a pattern to the pattern list; repeatable | |
--remove-pattern |
Remove a pattern from the pattern list; repeatable | |
--clear-files |
Remove all files from the entry | |
--clear-patterns |
Remove all patterns from the entry | |
--dry-run |
Preview without applying changes |
$ store modify nvim -t ~/.config/nvim-custom
$ store modify shells --clear-files -p ".zsh*" -p ".bash*"
$ store modify shells --add-file .zprofile
$ store modify shells --remove-file .bashrcHow it works:
- Removes old symlinks before applying the updated config.
- Flags compose in this order:
--clear-*→--files/--patterns→--add-*→--remove-*. This lets--add-fileappend to the existing list without replacing it, or to a replacement list supplied by--files. - Re-links the store after saving the new entry.
- Refuses to run on multi-target stores; use
store target modifyinstead.
Prints a one-line summary of every configured store without touching the filesystem. Useful for scripting or a quick glance.
$ store list nvim → ~/.config/nvim
shells (3 targets)
~
~/.config/fish
~/.config/nushell
git → ~/.config/git
Prints the absolute on-disk path of the store directory inside the repo. Designed for command substitution in shells.
$ cd $(store path nvim)Moves the store directory, updates the config entry, and re-links all targets under the new name.
$ store rename nvim vimRenamed store nvim → vim
Opens .store/config.yaml in $EDITOR (falls back to vi). Does not validate on close — run store doctor afterwards to catch syntax or reference errors.
$ store editAdds another target to an existing store. The target path may be passed positionally or via -t/--target.
| Flag | Short | Description |
|---|---|---|
--target |
-t |
Target path (or pass positionally) |
--files |
-f |
Explicit files to link individually; repeatable |
--patterns |
-p |
Glob patterns to match files; repeatable; supports ** |
--dry-run |
Preview without applying changes |
$ store target add shells ~/.config/fish -f config.fish
$ store target add shells -t ~/.config/nushell -p "*.nu"How it works:
- Automatically migrates a single-target store to the
targets:format. - Rejects duplicate target paths after normalizing
~and absolute paths. - Passing both a positional target and
--targetis an error; pick one.
Removes one target from a store and unlinks that target's symlinks.
| Flag | Short | Description |
|---|---|---|
--target |
-t |
Target path (or pass positionally) |
--dry-run |
Preview without applying changes |
$ store target remove shells ~/.config/fishremoved target ~/.config/fish from shells
How it works:
- Finds the target by normalized path.
- Unlinks that target.
- Migrates the store back to single-target format automatically if one target remains.
Updates the files or patterns for one target inside a multi-target store. Supports the same wholesale and incremental flags as store modify.
| Flag | Short | Description |
|---|---|---|
--target |
-t |
Target path (or pass positionally) |
--files |
-f |
Replace the file list; repeatable |
--patterns |
-p |
Replace the pattern list; repeatable |
--add-file |
Append a file to the file list; repeatable | |
--remove-file |
Remove a file from the file list; repeatable | |
--add-pattern |
Append a pattern to the pattern list; repeatable | |
--remove-pattern |
Remove a pattern from the pattern list; repeatable | |
--clear-files |
Remove all files from the target | |
--clear-patterns |
Remove all patterns from the target | |
--dry-run |
Preview without applying changes |
$ store target modify shells ~ -f .zshrc -f .bashrc -f .zprofile
$ store target modify shells ~ --add-file .zprofile
$ store target modify shells ~/.config/nushell --clear-files -p "*.nu"How it works:
- Removes old symlinks for the selected target first.
- Flag composition is the same as
store modify:--clear-*→ replace → add → remove. - Re-links only the updated target after saving config.
Removes the store's symlinked targets and deletes the store entry from config. With --all, removes every configured store.
| Flag | Description |
|---|---|
--all |
Remove every configured store |
--yes |
Skip the confirmation prompt when using --all |
--dry-run |
Preview without applying changes |
$ store remove nvim
$ store remove nvim --dry-run
$ store remove --all
$ store remove --all --yes$ store remove nvim
Removed store nvim (~/.config/nvim)
$ store remove --all
Remove ALL configured stores? [y/N] y
Removing all stores:
removed nvim (~/.config/nvim)
removed git (~/.config/git)
How it works:
- With a name, removes that one store's symlinks and config entry. The directory inside your repo is left untouched.
- With
--all, prompts for y/N unless--yesis passed, then runs globalpre-removeandpost-removehooks and removes every store. Continues removing other stores even if one fails, then reports aggregated errors. store removeallis a deprecated alias forstore remove --all --yesand prints a deprecation warning. Use the new form.
Shows the current symlink state for one store or all stores.
$ store status
$ store status nvim$ store status
nvim [linked] ~/.config/nvim
shells .zshrc [linked] ~/.zshrc
shells config.fish [missing] ~/.config/fish/config.fish
git [conflict] ~/.config/git
How it works:
- In file mode, each linked file is reported separately.
- When run without a name, stores filtered out by
when:are omitted.
Previews what store apply would do without changing anything.
| Flag | Short | Description |
|---|---|---|
--only |
-o |
Preview only the named stores (repeatable) |
$ store diff
$ store diff --only nvim --only git$ store diff
nvim ~/.config/nvim [ok]
shells .zshrc → ~/.zshrc [ok]
shells config.fish → ~/.config/fish/config.fish [create]
git ~/.config/git [conflict]
Summary: 2 ok, 1 to create, 1 conflict, 0 to replace
How it works:
- Uses the same platform filtering as
store apply. - Reports current targets as
ok,create,conflict,replace, orerror. replacemeans a broken symlink would be removed and recreated.
Runs diagnostics against the current repo and reports errors, warnings, and informational issues.
$ store doctorChecking store health...
[ok] 4 stores configured
[ok] all symlinks healthy
[info] directory "scratch" exists but is not configured as a store
[warn] secrets file not found but templates reference secrets
2 issues found (1 warning, 1 info)
What it checks:
- Config entries whose store directory is missing.
- Top-level directories in the repo that are not configured as stores.
- Broken symlinks.
- Multiple stores claiming the same target path.
- Missing secrets file or missing referenced secrets when
STORE_PASSPHRASEis available. - Empty store directories.
- Stores skipped on the current platform because of
when:.
How it works:
- Exits with a non-zero code only when doctor-level errors are found.
- Warnings and info are still printed but do not make the command fail.
Opens the interactive dashboard. See Interactive TUI for the full keymap, command palette, and destructive-action conventions.
$ store tuiPrints the current version.
$ store version
$ store --versionstore version <version>
If built without an injected version, the binary reports dev.
Creates or updates one encrypted secret.
$ store secret set github_token
$ store secret set api_key "super-secret"How it works:
- If
valueis omitted,storeprompts for it with hidden input. This is the recommended form; passing the value as an argument exposes it to shell history and process listings, andstoreprints a warning when you do. - Reads the passphrase from
STORE_PASSPHRASEor prompts interactively.
Prints one decrypted secret value.
$ store secret get github_tokenDeletes one secret from .store/secrets.enc. Aliased as store secret rm.
$ store secret remove github_token
$ store secret rm github_tokenLists secret names in sorted order.
$ store secret listHow it works (all secret commands):
- Secrets are stored in
.store/secrets.enc. - Secret names are listed; secret values are never listed.
- See Secrets for template rendering details.
Generates a shell completion script. See Shell Completion for install steps.
store completion generates completion scripts for supported shells. This is the easiest way to make the command and store-name arguments discoverable from the terminal.
$ mkdir -p ~/.local/share/bash-completion/completions
$ store completion bash > ~/.local/share/bash-completion/completions/store$ store completion zsh > "${fpath[1]}/_store"$ mkdir -p ~/.config/fish/completions
$ store completion fish > ~/.config/fish/completions/store.fish$ store completion powershell | Out-String | Invoke-ExpressionHow it works:
- The CLI generates shell-native completion scripts through Cobra.
- Existing store names are offered as tab completions for commands that take a configured store name, such as
modify,remove,status, andtargetsubcommands. - If your shell requires an explicit completion system bootstrap, enable that in the shell separately.
- Regenerate the script after upgrading
storeif you want completion descriptions and command trees to stay current.
| Status | Meaning |
|---|---|
[linked] |
A symlink exists and points to the expected source |
[missing] |
No symlink exists yet |
[conflict] |
A non-store file or directory exists where store wants to link |
[broken] |
A symlink exists but points to a missing source |
[drift] |
A rendered file exists at the target but its content has changed |
These statuses appear in store status, and the same underlying states drive store diff. store status also prints a summary line showing the count of each status across all reported stores.
Bare ~ is YAML null, not a string. Quote every ~ path:
stores:
shell:
target: "~"The same rule applies to paths such as "~/.config/nvim".
Run the command inside a repo initialized with store init, or initialize the current directory first.
store apply reports conflicts before linking and offers to move those files into the store.
The following files conflict with store symlinks:
~/.config/nvim (directory -> will be moved to ~/dotfiles/nvim)
~/.zshrc (file -> will be moved to ~/dotfiles/shells/.zshrc)
Move these files into the store and create symlinks? [y/N]
If files already exist inside the store where moved content would land, store asks before creating .bak backups. Use --force to skip that backup confirmation.
replace means the target is currently a broken symlink. Running store apply will remove it and recreate it correctly.
If templates reference secrets and .store/secrets.enc is missing, doctor warns immediately. If the secrets file exists, set STORE_PASSPHRASE before running store doctor if you want it to verify that referenced secret names are actually present.
store apply is idempotent — re-running it picks up where it left off. To narrow down what went wrong:
store statusshows what's linked and what isn't.store diffshows whatapplywould still change.store apply --only <name>runs one store in isolation so the error is easier to read.store doctorsurfaces configuration problems that may be causing the failure.
Creating symlinks on Windows requires either Developer Mode enabled (Settings → Privacy & Security → For developers) or running the terminal as Administrator. store doctor detects the missing capability on first run. Alternatively, run store from WSL, which has no such restriction.
when: filters compare against detected platform info. If a store isn't matching:
- Run
store doctor— it reports skipped stores as informational issues and shows which field didn't match. - Common causes: distro name doesn't match what you expect (e.g.
debianvsraspbian), WSL detection on an unusual setup, or a hostname that changed. - To inspect the detected values, write a quick hook that echoes them — per-store hooks receive
STORE_OS,STORE_ARCH,STORE_DISTRO,STORE_DISTRO_VERSION,STORE_HOSTNAME,STORE_SHELL, andSTORE_WSLin the environment.
Set STORE_PASSPHRASE in the environment before running store apply or store doctor to avoid interactive prompts. With the variable set, store doctor additionally verifies every referenced secret is present in the vault. An unset or wrong passphrase causes template rendering to fail loudly — no silent fall-through, no stale output.
store path <name> prints the absolute on-disk path of a store and nothing else — no ANSI, no log output. That makes it safe for command substitution inside aliases and pipelines:
$ cd $(store path nvim)
$ ls -la $(store path shells)