Skip to main content

haunt

A dotfiles symlink manager. GNU stow in Python, with a registry.

Installation

# Run directly with uvx (no install needed)
uvx haunt install ~/.dotfiles

# Or install globally with uv
uv tool install haunt

# Or install with pip
pip install haunt

Dependencies

We recommend installing haunt with uv, which installs Python, haunt and its dependencies.

If you choose in install manually, haunt requires Python 3.12+.

Quickstart

You have a home directory with some existing files, and a dotfiles package:

/Users/mike
├── .config
│   └── starship.toml
└── dotfiles
    ├── .bashrc
    └── .config
        └── nvim
            └── init.lua

A package is just a directory containing files you want to link to from your home directory.

To install the package:

$ haunt install ~/dotfiles
Creating symlinks:
  /Users/mike/.bashrc -> /Users/mike/dotfiles/.bashrc
  /Users/mike/.config/nvim/init.lua -> /Users/mike/dotfiles/.config/nvim/init.lua

2 symlinks created

The files in the package are symlinked from your home folder, and existing files are preserved.

/Users/mike
├── .bashrc -> dotfiles/.bashrc
├── .config
│   ├── nvim
│   │   └── init.lua -> ../../dotfiles/.config/nvim/init.lua
│   └── starship.toml
└── dotfiles
    ├── .bashrc
    └── .config
        └── nvim
            └── init.lua

Uninstall the package (using the package name, which is the directory basename):

$ haunt uninstall dotfiles
Removing symlinks:
  /Users/mike/.bashrc
  /Users/mike/.config/nvim/init.lua

2 symlinks removed

To see all installed packages, use haunt list.

Commands

haunt install

haunt install [OPTIONS] PACKAGE [TARGET]
  • PACKAGE - directory containing files to symlink (required)
  • TARGET - where to create symlinks (default: $HOME)
  • --dry-run, -n - show what would happen without doing it
  • --on-conflict - how to handle conflicts:
    • abort (default) - stop if any files exist
    • skip - skip conflicting files, install the rest
    • force - replace files/symlinks (but never directories)

haunt uninstall

haunt uninstall [OPTIONS] PACKAGE
  • PACKAGE - package name to uninstall (required)
  • --dry-run, -n - show what would happen without doing it

Package names are derived from the directory basename. For example, haunt install ~/dotfiles creates a package named dotfiles. To see all installed packages, use haunt list.

haunt list

haunt list [OPTIONS] [PACKAGE]

List installed packages with their symlinks.

  • PACKAGE - show only this package (optional, shows all if omitted)
  • --verbose, -v - show all symlinks with status validation

Example output:

$ haunt list
dotfiles
  Package: ~/dotfiles
  Target: ~/
  Installed: 2025-11-12 13:45:23
  Symlinks: 3

nvim-config
  Package: ~/nvim-config
  Target: ~/.config
  Installed: 2025-11-12 14:30:15
  Symlinks: 5

Verbose mode checks each symlink and reports issues:

$ haunt list --verbose dotfiles
dotfiles
  Package: ~/dotfiles
  Target: ~/
  Installed: 2025-11-12 13:45:23
  Symlinks:
    Correct
      ~/.bashrc -> ~/dotfiles/.bashrc
    Inconsistent with Registry
      ~/.vimrc -> ~/dotfiles/.vimrc (link missing)
      ~/.zshrc -> /other/file (expected ~/dotfiles/.zshrc)
      ~/.profile -> ~/dotfiles/.profile (source file missing)

  To fix inconsistent symlinks:
    haunt install ~/dotfiles ~/

Inconsistent symlink types:

  • (link missing): symlink doesn't exist at expected location
  • (expected ...): symlink points to wrong target
  • (source file missing): symlink exists but source file is gone

Conflict handling

By default haunt aborts on any conflict:

$ echo "important config" > ~/.bashrc
$ haunt install ~/.dotfiles
✗ Conflicts detected:
  /Users/mike/.bashrc (file)

Use --on-conflict=skip to install non-conflicting files, or --on-conflict=force to replace files and broken symlinks.

haunt will never replace existing directories with symlinks, even with --on-conflict=force.

Multiple packages

haunt creates symlinks to files, not directories. This lets multiple packages install into the same directory:

/Users/mike/dotfiles
├── shell
│   ├── .bashrc
│   └── .config
│       └── starship.toml
└── nvim
    └── .config
        └── nvim
            └── init.lua

Install both packages:

$ haunt install ~/dotfiles/shell
Creating symlinks:
  /Users/mike/.bashrc -> /Users/mike/dotfiles/shell/.bashrc
  /Users/mike/.config/starship.toml -> /Users/mike/dotfiles/shell/.config/starship.toml

2 symlinks created

$ haunt install ~/dotfiles/nvim
Creating symlinks:
  /Users/mike/.config/nvim/init.lua -> /Users/mike/dotfiles/nvim/.config/nvim/init.lua

1 symlink created

Result:

/Users/mike
├── .bashrc -> dotfiles/shell/.bashrc
└── .config
    ├── nvim
    │   └── init.lua -> ../../dotfiles/nvim/.config/nvim/init.lua
    └── starship.toml -> ../dotfiles/shell/.config/starship.toml

Both packages install files into .config. haunt creates real directories (not symlinks to directories), so this works fine.

Uninstalling one package leaves the other intact:

$ haunt uninstall shell
Removing symlinks:
  /Users/mike/.bashrc
  /Users/mike/.config/starship.toml

2 symlinks removed

# .config/nvim/init.lua remains untouched

Adding and removing files from packages

To add a file to an already-installed package, copy or move it into the package directory (creating subdirectories as needed), then reinstall:

# Add a simple file
mv ~/.vimrc ~/dotfiles/.vimrc
haunt install ~/dotfiles

# Add a nested file (create directories first)
mkdir -p ~/dotfiles/.config/nvim
mv ~/.config/nvim/init.lua ~/dotfiles/.config/nvim/init.lua
haunt install ~/dotfiles

The reinstall detects the file is now missing from its original location and creates the symlink.

To remove a file from a package, delete it from the package directory, then reinstall:

rm ~/dotfiles/.vimrc
haunt install ~/dotfiles

The reinstall automatically removes symlinks for files that are no longer in the package.

Git Integration

If your package is in a git repository, haunt automatically uses git ls-files to discover files. This means:

  • ✅ .gitignore rules are respected automatically
  • ✅ .git directory and .gitmodules are excluded
  • ✅ Only tracked files are symlinked
  • ✅ Files in submodules are discovered

For non-git packages (or if git is not available), haunt falls back to discovering all files in the directory tree.

The Registry

haunt maintains state about the links it manages independently of the package directory. This means:

  • ✅ haunt uninstall works even if the package directory was moved, deleted, or its contents modified
  • ✅ Won't remove symlinks that have been manually modified to point elsewhere or replaced with files

If you need to uninstall a package that's not in the registry: Run haunt install <package-dir> first to detect existing symlinks and rebuild the registry entry, then haunt uninstall <package-name> will work normally.

The registry follows the XDG Base Directory specification:

  • Linux: ~/.local/state/haunt/registry.json (or $XDG_STATE_HOME/haunt/registry.json)
  • macOS: ~/Library/Application Support/haunt/registry.json

How is this different from GNU stow?

GNU stow is the original. It's mature, battle-tested, and does folder tree merging.

The main differences:

  • No Perl dependency - uses Python (ubiquitous on modern systems) and uv
  • Registry - see The Registry section for benefits

The core symlinking behavior is the same as stow.

How is this different from stowsh?

stowsh is my bash implementation of stow.

haunt trades stowsh's zero dependencies for a registry, maintainability and testability. If you want a single bash script with no dependencies, use stowsh!

Development

See CONTRIBUTING.md for development setup.

License

MIT

Release files for haunt 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for haunt 0.2.0
File Size Uploaded
haunt-0.2.0.tar.gz 45.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for haunt 0.2.0
File Interpreter ABI Platform
haunt-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 65.4 kB

Release files / haunt-0.2.0.tar.gz

Download URL haunt-0.2.0.tar.gz
Size 45.0 kB
Tags Source
SHA-256 checksum
How to use checksums
99873d6dcf212d5d562565832bb534a6c58a805e653916fb6593958742589272
BLAKE2b-256 checksum
How to use checksums
7901405ad550284e57dd5b28b8df4ca4300b4b6d190751a9cb3ab68cffd1f136
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 14, 2025.

Transparency log

Release files / haunt-0.2.0-py3-none-any.whl

Download URL haunt-0.2.0-py3-none-any.whl
Size 20.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bdc3c47c04d24d4126bcfa7f98161ab1584ab4e665f346d10c21fdd448a53cc8
BLAKE2b-256 checksum
How to use checksums
0072cdea41eb61c078cf436e8449f48f584ffa4e3e1cae123bb0f230ad459684
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 14, 2025.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page