Skip to content

About

Manage your dotfiles and setup scripts from a git repository.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

1 watching

Forks

configfile

Release npm CI npm GitHub Sponsors

configfile is a command line tool that helps you manage your configuration files (dotfiles) and setup scripts from a git repository.

Requirements

  • macOS or Linux
  • Node.js 24.11 or later (Homebrew installs it for you)
  • git (Homebrew installs it for you; otherwise, when it is missing, configfile says how to install it on your system)

Installation

With Homebrew, on macOS or Linux (Node.js and git come with it):

brew install mindsers/tap/configfile

With npm, when Node.js 24.11 or later is installed, from the GitHub release:

npm install --global https://github.com/mindsers/configfile/releases/download/1.0.0/configfile-1.0.0.tgz

Install it one way only: two copies would compete in your PATH.

Version 1.0 is not on the npm registry yet: npm install --global configfile still installs 0.3.1. Each release's tarball comes with a SHA256SUMS file and a build provenance attestation, which gh attestation verify configfile-1.0.0.tgz --repo mindsers/configfile checks.

Data storage

This tool does not store configuration files for you. A git repository (dotfiles) is needed to store your configuration files.

You edit your dotfiles in your own working copy of that repository and push them. configfile keeps its own copy, a mirror of the remote repository in ~/.configfile/dotfiles, and deploys from it: configfile update makes the mirror identical to the remote, so unpushed or conflicting work never blocks it. Don't edit the mirror (or deployed links, which point into it): see update for what happens to such changes.

The repository must have this structure:

files/
    zsh/
        settings.json
        zshrc
    git/
        settings.json
        gitconfig
scripts/
    setup.sh
    macos/
        index.sh

Modules

Every folder of files/ (or symbolic link to a folder) that contains a settings.json is a module. Its name is the folder name, lowercased, with spaces replaced by - and other characters than ASCII letters, digits, _ and - removed: My Zsh.d is the my-zshd module. Hidden folders are ignored.

settings.json lists the files of the module:

{
  "files": [
    { "source_path": "zshrc", "target_path": "~/.zshrc", "deploy": "global" },
    { "source_path": "editorconfig", "target_path": ".editorconfig", "deploy": "local" },
    { "source_path": "old-aliases", "target_path": "~/.aliases", "deploy": "none" }
  ]
}
Key Description
source_path Path of the file (or folder), relative to the module folder. It must stay inside the module folder, symbolic links included.
target_path Where the file is deployed. ~ is your home folder. A relative path is relative to your home folder for global files (.zshrc is ~/.zshrc) and to the current folder for local files. Targets can be anywhere you can write, except: your home folder, the current folder, the dotfiles repository, the module folder, configfile's own files (~/.configfilerc, ~/.configfile/), one of their parents, or anything inside the repository. These are recognised whatever the path used to reach them (letter case, symbolic links).
deploy "global": the file is symlinked by configfile modules deploy. "local": the file is copied by configfile modules deploy --local, typically into a project folder (symbolic links in the source are followed, so the copy never points into the repository). "none": the file is never deployed, which keeps it in the repository for later.
global Deprecated, removed in 2.0: older spelling of deploy. true is "global", false is "local". It still works in 1.x, with a warning. Use one or the other, not both.

An entry without deploy or global is not deployed, and modules deploy warns about it.

When a global file is deployed and something else already exists at its target (a file, a folder or another link), it is moved to <target>.old (or <target>.old.1, …). A link that already points to the right file is left as is, so deploying twice is safe.

When a local file already exists and differs from the one in the repository, configfile asks whether to replace it, once the other files are deployed. The existing file or folder is then moved to <target>.old (or .old.1, …) before the copy, so nothing is lost.

configfile records what it deploys and the backups it makes in ~/.configfile/state.json. configfile modules undeploy reverts a deployment: it removes the links that point to the repository's files and the local copies configfile made, as long as a copy is still the file it made and matches the repository's current version, and moves the most recent backup it made back in place, if that backup is unchanged. Anything else is left untouched: files it did not create (even when identical to the repository), apart from links to the repository's files, local copies that differ from the repository (changed by you, or because the repository changed since) or were saved again by an editor, and .old files you made yourself.

When an entry leaves the repository (removed, set to "deploy": "none", given another target_path, or its module deleted), what configfile deployed for it stays in place until it is undeployed: modules status lists these files, update warns about them, and modules undeploy --removed (or --all) removes them and restores the files they replaced. Local copies are handled from the folder they were copied into, like modules deploy --local. A removed file that configfile cannot remove safely (replaced by your own file, a modified copy, a copy whose source is gone) is left where it is, and configfile stops tracking it.

To stay safe, configfile never undeploys a file when it cannot tell whether the repository still deploys it: files of a module whose settings.json cannot be used or has invalid entries, of a module folder that is not usable (a broken symbolic link, a name another folder uses), and entries without a deployment strategy. modules status lists them separately. Links deployed by 0.3 and not deployed again since were not recorded, so they are not found either.

modules deploy and modules undeploy change files one at a time across processes: while one runs, another one (except a dry run) waits for it, up to 10 seconds, then stops with an error (the lock is ~/.configfile/lock). Other commands, such as update, don't wait for it.

Deploying a repository means trusting it, like code you run: its files end up in your shell configuration, and its scripts run on your machine. Only deploy repositories you trust.

Scripts

Every file of scripts/, and every folder of scripts/ (or symbolic link to a folder) containing an index file (index, index.sh, …), is a script. Hidden files are ignored. To only use some extensions, set script_extensions in the configuration.

A script's name is its file name (or folder name) up to the first dot, with the same rules as module names: scripts/setup.sh is named setup. When two scripts have the same name, only the first one, alphabetically, is used, with a warning.

A script can have a version for each system: macos or linux after the first dot (scripts/setup.macos.sh, scripts/setup.linux.py, or a folder such as scripts/setup.macos/). On macOS, configfile scripts run setup runs setup.macos.sh; on another system, it runs the generic setup.sh, if there is one. A version for another system is ignored.

Scripts can be written in any language:

  • a script with a shebang line (such as #!/usr/bin/env python3) is run directly when it is executable, and with that interpreter otherwise;
  • a .js (or .mjs, .cjs) or .sh script without shebang line is run with node or sh;
  • any other executable file (such as a compiled program) is run directly.

configfile never changes the permissions of your files. Scripts run in the current folder, their output is not modified (the messages of configfile go to stderr), and their exit code is forwarded.

Scripts get these environment variables, in addition to configfile's own environment (they replace variables of the same name):

Variable Value
CONFIGFILE_REPO the full path of the dotfiles repository (folder_path), to reach its files: "$CONFIGFILE_REPO/Brewfile"
CONFIGFILE_SCRIPT the script's name (setup)
CONFIGFILE_OS the system: macos or linux, as in the names of script versions

Usage

  • configfile init (i): ask for the URL of your dotfiles repository (any URL git can clone, such as https://github.com/me/dotfiles.git or git@github.com:me/dotfiles.git, or an existing local folder), clone it into ~/.configfile/dotfiles and save the configuration in ~/.configfilerc. If the folder already contains a git repository, it is used as is. When you run init again, the existing folder is kept.
    • -f, --force: overwrite an existing configuration without asking.
    • --repo <url>: give the URL from the command line, for non-interactive setups.
    • --folder <path>: clone somewhere else than ~/.configfile/dotfiles.
  • configfile modules list (m l, or just configfile modules): list available modules.
  • configfile modules status [modules...] (m st): show whether each global file of the modules (all modules by default) is deployed, not deployed, or blocked by another file. Without module names, it also lists the deployed files the repository no longer deploys.
    • -l, --local: check the local files, in the current folder.
  • configfile modules deploy [modules...] (m d): deploy the global files of the given modules. Without module names, asks to deploy all modules.
    • -l, --local: copy the local files of the modules instead.
    • -a, --all: deploy every module without asking.
    • -f, --force: replace existing local files without asking (they are moved to .old).
    • -n, --dry-run: show what would be done, without changing anything.
  • configfile modules undeploy [modules...] (m u): remove the deployed global files of the given modules and restore their backups. Without module names, asks to undeploy all modules.
    • -a, --all: undeploy every module without asking, and the files the repository no longer deploys.
    • --removed: only undeploy the files the repository no longer deploys (see Modules); with --local, the copies made in the current folder.
    • -l, --local and -n, --dry-run: as for deploy.
  • configfile scripts list (s l, or just configfile scripts): list available scripts.
  • configfile scripts run <name> [-- args...] (s r): run a script. Arguments after -- are passed to the script.
  • configfile update (u): sync configfile's copy of your dotfiles repository with the remote: it fetches the remote and makes the copy identical to it, whatever happened to the copy. Local changes found in the copy (uncommitted edits, new files or unpushed commits, for example edits made through a deployed link) are first saved as a patch in ~/.configfile/saved/; apply it in your own working copy with git am <patch> to keep them. Global files are symbolic links, so they are up to date right away; run modules deploy --all for new files, and modules deploy --local to refresh local copies. It warns about deployed files the repository no longer deploys.
  • configfile history: show what configfile changed, most recent last (see History).
    • -n, --limit <count>: number of runs to show (20 by default).
    • --json: print the raw history lines (JSON Lines), for scripts.

configfile exits with a non-zero code when a command fails, and with the script's exit code when a script fails, so it can be used from other scripts. When stdin is not a terminal, a command that would ask a question fails and names the option to pass instead (--repo, --folder and --force for init; module names or --all for modules deploy and undeploy). Without a terminal, modules deploy --local skips existing local files and exits with an error, unless --force is given. Ctrl+C at a question exits with code 130. Each run of a command that changes files, and any unexpected error, is also recorded in the history.

Configuration

configfile keeps its configuration in ~/.configfilerc, and its working files in the ~/.configfile/ folder: the mirror of your repository (dotfiles/), the record of deployments (state.json), the history (history.jsonl), the lock, and saved local changes (saved/).

~/.configfilerc is a JSON file, readable by you only: the repository URL may contain credentials, so if other users can read it, configfile makes it private and warns you.

{
  "repo_url": "git@github.com:me/dotfiles.git",
  "folder_path": "/Users/me/.configfile/dotfiles",
  "script_extensions": [".js", ".sh", ""],
  "history_max_size": "1MB"
}

folder_path may start with ~; a relative path is relative to your home folder. script_extensions is optional: without it, every file of scripts/ is a script. With it, only files with one of these extensions are; the dot may be omitted ("py"), and "" means files without extension. history_max_size is optional: see History.

History

configfile records what each command changed, when, and whether it failed, in ~/.configfile/history.jsonl: one JSON line per run of init, modules deploy, modules undeploy, update and scripts run (dry runs and usage errors excepted), and per unexpected error of any command. Each line has the time, the configfile version, the command and its options, the current folder, the exit code, the error message, and the changes: files linked, copied, moved aside to .old, removed, restored, kept or skipped, syncs (from which commit to which, and saved patches), scripts and their exit codes.

Script arguments, environment variables and file contents are never recorded. In URLs, user names and passwords (https://user:token@host) and query parameters that look like secrets (?private_token=…) are hidden. Paths are recorded in full, so they include your user name. When configfile creates the file, only its owner can read it. A run stopped by a signal (Ctrl+C outside a question) is not recorded, unless the signal comes while a script or git runs: configfile then leaves it to that program, and records the run with its exit code.

configfile history shows the last runs:

2026-10-01 11:12  modules deploy zsh  ok
  linked    ~/.zshrc  (previous file moved to ~/.zshrc.old)
  2 unchanged

2026-10-01 11:15  update  exit 128
  error     git fetch failed (exit code 128).

-n <count> shows another number of runs, and --json prints the lines as they are written.

The file is also easy to query with jq, for example to list the failed runs:

jq 'select(.exitCode != 0)' ~/.configfile/history.jsonl

When the file reaches history_max_size (1MB by default; a number of bytes, or a size such as "512KB" or "5MB" in ~/.configfilerc), it is renamed to history.1.jsonl, replacing the previous one, so the history takes about twice that size at most. "history_max_size": 0 turns the history off. An invalid value gives the default and a warning; when ~/.configfilerc cannot be read, nothing is recorded.

Upgrading from 0.3

  • Node.js 24.11 or later is required, on macOS or Linux. Install again as described in Installation. To switch to Homebrew, remove the npm version first (npm uninstall --global configfile).
  • ~/.configfilerc, the repository layout and settings.json work as before, including settings.json files that are a plain list (the 0.3.1 format, deprecated: put the list in a "files" key). Links deployed by 0.3 are recognised as deployed.
  • If you installed 0.3 with Yarn, remove it first: yarn global remove configfile.
  • "global": true | false still works but is deprecated: replace it with "deploy": "global" | "local".
  • A relative target_path of a global file is now relative to your home folder, not to the folder you run configfile from. Targets starting with ~/ or / are not affected.
  • Scripts keep their names (up to the first dot) and every file of scripts/ is still a script. They are no longer made executable by configfile: a script needs a shebang line, a .js/.sh extension, or to be executable.
  • Targets inside the dotfiles repository, or containing it, are now refused.
  • modules undeploy also removes the links made by 0.3 that point to the repository, but it only restores the backups it made itself (recorded in ~/.configfile/state.json): the .old files made by 0.3 are not put back, and local copies made by 0.3 are kept.
  • A source_path must stay inside its module folder.
  • init clones into ~/.configfile/dotfiles by default, and an existing folder_path is kept. update now makes that folder identical to the remote, saving local changes as a patch first. If your configured folder is also your working copy (for example ~/.dotfiles), give configfile its own copy: run configfile modules undeploy --all (files that 1.0 moved aside come back; links made by 0.3 are removed, and their .old files stay where they are), then configfile init --force --repo <url> --folder ~/.configfile/dotfiles, then configfile modules deploy --all (links now point to the copy).
  • With script_extensions set, only files with one of these extensions are scripts, and "" means files without extension (0.3 matched any file containing the text).
  • Commands now exit with a non-zero code on failure.

Contribution

Contributions are welcome and greatly appreciated. Read How to contribute to configfile before opening a pull request, and follow the code of conduct.

Support

configfile is free and open source software, under the Apache-2.0 license, built on my free time.

If you use it and want to support its development, you can sponsor me on GitHub Sponsors. Thank you!

The people who support configfile are listed in SPONSORS.md.

License

This project is under the Apache-2.0 license. See the LICENSE file.

About

Manage your dotfiles and setup scripts from a git repository.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages