Winstow

Winstow is a Windows-native symlink farm manager inspired by GNU Stow. It helps you manage your dotfiles, configuration files, and software packages by creating symbolic links from a central stow directory to a target directory (typically your home directory).
Disclaimer
I created Winstow to replicate my GNU Stow dotfile management workflow from Linux on Windows - keeping all my configuration files in a single git repository and symlinking them as needed.
⚠️ Note: Winstow is currently in active development and may be unstable. The API is subject to change without prior notice. Please use it with caution and make backups of your files before using it.
I'm still learning Rust, so the code may not be the most idiomatic or efficient. Suggestions for improvement are welcome.
⚠️ Winstow is not a 1:1 port of GNU Stow and does not guarantee identical range of functionality or behavior.
Why Winstow?
- ✅ Windows-Native: Built specifically for Windows with proper symlink support
- ✅ GNU Stow Compatible: Familiar interface if you've used GNU Stow
- ✅ Directory Folding: Intelligent directory symlinking reduces link count
- ✅ Conflict Resolution:
--adopt and --override for handling existing files
- ✅ Pattern Matching:
--ignore and --defer for flexible file management
- ✅ Dotfiles Support:
--dotfiles to stow dot-foo files as .foo (like GNU Stow)
- ✅ Configuration Files: Set defaults with
.winstowrc
- ✅ Small & Fast: Single ~600KB binary
- ✅ Safe: Dry-run mode to preview changes before applying
Prerequisites
To create symbolic links on Windows, you need one of the following:
-
Developer Mode enabled (Windows 10/11) - Recommended
- Settings → Update & Security → For developers → Developer Mode
-
Administrator privileges
- Run winstow from an elevated command prompt
Installation
Option 1: Download Binary
Download the latest release from GitHub Releases and add it to your PATH.
Option 2: Build from Source
# Clone the repository
git clone https://github.com/MathiasCodes/winstow.git
cd winstow
# Build release binary
cargo build --release
# Binary will be at target/release/winstow.exe
Option 3: Package Managers
# WinGet
winget install winstow
Quick Start
Basic Usage
# Stow a package (create symlinks)
winstow mypackage
# Unstow a package (remove symlinks)
winstow -D mypackage
# Restow a package (refresh symlinks)
winstow -R mypackage
# Dry-run to see what would happen
winstow -n mypackage
# Verbose output
winstow -v mypackage
Directory Structure
# Stow directory (contains all your dotfile packages)
C:\Users\USER\Dotfiles\
├── Git\
│ └── .gitconfig
├── Git-Bash\
│ ├── .bashrc
│ └── .inputrc
└── lazygit\
└── AppData\
└── Local\
└── lazygit\
└── config.yaml
# Target directory (where symlinks are created, typically your home directory)
# Note: lazygit uses directory folding - a single directory symlink instead of individual file symlinks
C:\Users\USER\
├── .gitconfig -> C:\Users\USER\Dotfiles\Git\.gitconfig
├── .bashrc -> C:\Users\USER\Dotfiles\Git-Bash\.bashrc
├── .inputrc -> C:\Users\USER\Dotfiles\Git-Bash\.inputrc
└── AppData\
└── Local\
└── lazygit -> C:\Users\USER\Dotfiles\lazygit\AppData\Local\lazygit
Command-Line Interface
Actions
| Flag | Long Form | Description |
|---|
-S (optional) | --stow | Stow packages (default action) |
-D | --delete | Unstow (delete) packages |
-R | --restow | Restow packages (unstow then stow) |
Options
| Flag | Long Form | Description |
|---|
-d DIR | --dir DIR | Stow directory (default: current directory) |
-t DIR | --target DIR | Target directory (default: home directory) |
-v | --verbose | Enable verbose output |
-n | --dry-run | Preview changes without applying them |
| --adopt | Move conflicting files into package (stow/restow only) |
| --override | Remove conflicting files and directories (stow/restow only, destructive) |
| --no-folding | Create real directories instead of directory symlinks, never refold on unstow |
| --ignore PATTERN | Skip files matching pattern (stow/restow only) |
| --defer PATTERN | Skip files matching pattern if they already exist in target (stow/restow only) |
| --dotfiles | Map dot-foo to .foo during stow, unstow, and restow |
-h | --help | Show help message |
-V | --version | Show version |
Examples
Managing Dotfiles
PowerShell:
# Set up your stow directory
mkdir $env:USERPROFILE\Dotfiles
cd $env:USERPROFILE\Dotfiles
mkdir Git
mkdir Git-Bash
# Move your dotfiles into the packages
move $env:USERPROFILE\.gitconfig Git\
move $env:USERPROFILE\.bashrc Git-Bash\
move $env:USERPROFILE\.inputrc Git-Bash\
# Stow the packages
winstow -d $env:USERPROFILE\Dotfiles -t $env:USERPROFILE Git
winstow -d $env:USERPROFILE\Dotfiles -t $env:USERPROFILE Git-Bash
# Now .gitconfig, .bashrc, and .inputrc are symlinks to your Dotfiles packages
Git Bash:
# Set up your stow directory
mkdir -p $USERPROFILE/Dotfiles
cd $USERPROFILE/Dotfiles
mkdir Git
mkdir Git-Bash
# Move your dotfiles into the packages
mv $USERPROFILE/.gitconfig Git/
mv $USERPROFILE/.bashrc Git-Bash/
mv $USERPROFILE/.inputrc Git-Bash/
# Stow the packages
winstow -d $USERPROFILE/Dotfiles -t $USERPROFILE Git
winstow -d $USERPROFILE/Dotfiles -t $USERPROFILE Git-Bash
# Now .gitconfig, .bashrc, and .inputrc are symlinks to your Dotfiles packages
Stowing Files with dot- Names
Some people version-control their dotfiles without the leading dot (git ignores links
to its own dotfiles inside the repo, and files without a leading dot are easier to see
in editors/explorers). With --dotfiles, files and directories named dot-foo are
stowed as .foo, matching GNU Stow behavior:
# Package layout (no leading dots)
mypackage/
├── dot-gitconfig
├── dot-bashrc
└── dot-config/
└── nvim/
└── init.lua
# Stow with translation
winstow --dotfiles mypackage
# Result: ~/.gitconfig, ~/.bashrc, ~/.config/nvim/init.lua are symlinks
When unstowing, use the same flag so the links are found and removed:
winstow --dotfiles -D mypackage
Note: --dotfiles must be passed consistently for both stow and unstow — otherwise
package entry dot-bashrc would be looked up as the literal name dot-bashrc in the
target and the .bashrc link would not be removed. Files that already start with .
(e.g. .gitignore) are left unchanged.
Ignore and defer patterns match names in the package, before translation. For
example, use --ignore dot-secret to omit dot-secret, not --ignore .secret.
If a directory contains a name that needs translation or an ignored entry,
winstow creates a real target directory and links its eligible contents
individually. The directory's own name can still be translated and folded:
dot-config can link to .config when its children need no translation.
If two package entries map to the same target name, winstow reports a conflict
before changing anything, including with --override.
Handling Conflicts
# If files already exist in target, you'll get an error listing all conflicts
winstow mypackage
# Error: Conflict: the following paths already exist ...
# - C:\Users\You\.gitconfig
# Option 1: Adopt the existing file into the package
winstow --adopt mypackage
# Moves .gitconfig into mypackage/, then creates symlink
# Option 2: Override (remove) the existing file
winstow -n --override mypackage # preview what would be removed
winstow --override mypackage
# Removes .gitconfig, then creates symlink (destructive!)
winstow checks a package for conflicts before changing anything. If a conflict
cannot be resolved, it reports every conflicting path and makes no changes, not
even the moves and removals of --adopt and --override.
--adopt only adopts real files in place of package files. A file in place of
a package directory, or a symlink, remains a conflict.
--override removes conflicting files and directories, including their
contents, and replaces links of other packages. Unlike GNU Stow, it is not
limited to links of other packages. Symlinks pointing outside the stow
directory are never removed; they remain a conflict.
Using Ignore and Defer Patterns
# Ignore backup files and OS metadata (always skip these)
winstow --ignore "*.bak" --ignore ".DS_Store" --ignore "Thumbs.db" mypackage
# Ignore entire directories
winstow --ignore "node_modules" mypackage
# Defer allows another package to manage shared files
# If .bashrc already exists, skip it; otherwise, stow it
winstow --defer ".bashrc" package-a
# Later, package-b can manage .bashrc
winstow package-b # This will stow .bashrc from package-b
Difference between --ignore and --defer:
--ignore: Always skip files matching the pattern (e.g., temporary files, build artifacts)
--defer: Skip files matching the pattern only if they already exist in the target directory (useful for shared configuration files managed by different packages)
Restowing After Updates
PowerShell:
# After updating your dotfiles repository
cd $env:USERPROFILE\Dotfiles
git pull
# Refresh symlinks
winstow -R -d $env:USERPROFILE\Dotfiles -t $env:USERPROFILE Git
Git Bash:
# After updating your dotfiles repository
cd $USERPROFILE/Dotfiles
git pull
# Refresh symlinks
winstow -R -d $USERPROFILE/Dotfiles -t $USERPROFILE Git
Dry-Run Mode
PowerShell:
# See what would happen without making changes
winstow -n -v Git
# Output:
# === DRY RUN MODE - No changes will be made ===
# [DRY RUN] Create file link: .gitconfig -> ..\Dotfiles\Git\.gitconfig
# Would stow 1 package(s)
Git Bash:
# See what would happen without making changes
winstow -n -v Git
# Output:
# === DRY RUN MODE - No changes will be made ===
# [DRY RUN] Create file link: .gitconfig -> ..\Dotfiles\Git\.gitconfig
# Would stow 1 package(s)
Configuration File
Create a .winstowrc file in one of these locations:
- Current directory:
./.winstowrc
- Home directory:
%USERPROFILE%\.winstowrc
- AppData:
%APPDATA%\winstow\config.toml
Example Configuration
# Default stow directory
default-dir = "C:\\stow"
# Default target directory
default-target = "C:\\Users\\YourName"
# Default ignore patterns
ignore = ["*.bak", ".DS_Store", "Thumbs.db", "desktop.ini"]
# Default defer patterns
defer = ["*.lock"]
# Enable verbose mode by default
verbose = false
# Create real directories instead of directory symlinks
no-folding = false
# Enable dotfiles translation by default (--dotfiles)
dotfiles = false
CLI arguments always override config file settings.
Directory Folding
winstow implements directory folding (inspired by GNU Stow) for efficiency:
Folding (Creating a Single Directory Symlink)
# Package structure:
C:\Users\USER\Dotfiles\mypackage\AppData\Local\app\settings.json
# If C:\Users\USER\AppData\Local\app\ doesn't exist, winstow creates:
C:\Users\USER\AppData\Local\app -> C:\Users\USER\Dotfiles\mypackage\AppData\Local\app
Unfolding (Expanding When Needed)
# First package creates a directory symlink:
C:\Users\USER\AppData\Local -> C:\Users\USER\Dotfiles\package1\AppData\Local
# Second package needs different AppData\Local contents:
# winstow automatically unfolds:
C:\Users\USER\AppData\Local\
├── app1 -> C:\Users\USER\Dotfiles\package1\AppData\Local\app1
└── app2 -> C:\Users\USER\Dotfiles\package2\AppData\Local\app2
Refolding (Collapsing When Possible)
When a package is unstowed and a directory is left containing only links into
a single other package's directory, winstow replaces it with a single directory
symlink again. In the example above, unstowing package2 turns
C:\Users\USER\AppData\Local back into a link to package1\AppData\Local.
Disabling Folding
With --no-folding (or no-folding = true in .winstowrc), winstow creates
real directories and links only files. This is useful when applications write
their own files into a configuration directory that should not end up in the
package.
C:\Users\USER\AppData\Local\app\ (real directory)
└── settings.json -> C:\Users\USER\Dotfiles\mypackage\AppData\Local\app\settings.json
Best Practices
- Always use
-n first to preview changes before applying them
- Use
-v for debugging to see detailed operations and understand what's happening
- Set up
.winstowrc for common settings to avoid repeating command-line options
- Keep packages focused - one purpose per package (e.g., separate vim, git, bash packages)
- Test in a temp directory before applying to your real dotfiles
- Use version control (git) in your stow directory to track changes
- Document your packages with README files explaining what each package contains
- Backup before
--override - this option is destructive and removes existing files!
- Backup before
--adopt - this option is destructive and removes existing files!
Troubleshooting
"Permission denied" Error
Problem: Cannot create symbolic links
Solutions:
- Enable Developer Mode (Settings → For developers)
- Run as Administrator
- Check Windows version (requires Windows Vista+)
Symlinks Not Working
Problem: Symlinks appear as files or don't work
Check:
- Developer Mode is enabled OR running as Administrator
- Using proper Windows symlink support (not shortcuts)
- Target files/directories exist
"Conflict" Error
Problem: Conflict: path already exists
Solutions:
# Option 1: Adopt existing file
winstow --adopt mypackage
# Option 2: Override existing file
winstow --override mypackage
# Option 3: Manually remove/backup the file
move conflicting-file conflicting-file.backup
winstow mypackage
Dry-Run Shows Different Results
Problem: Dry-run output doesn't match actual execution
Note: Dry-run simulates operations but doesn't account for all dynamic conditions. Always make sure you have a backup of your files.
Development
Building
# Development build
cargo build
# Release build (optimized)
cargo build --release
# Run tests
cargo test
# Run tests including ignored ones (requires Developer Mode)
cargo test -- --ignored
Project Structure
winstow/
├── src/
│ ├── main.rs # Entry point and CLI routing
│ ├── cli.rs # Command-line argument parsing
│ ├── config.rs # Configuration file handling
│ ├── error.rs # Error types
│ ├── logger.rs # Logging infrastructure
│ ├── path_utils.rs # Path manipulation utilities
│ ├── fs_ops.rs # Windows filesystem operations
│ ├── planner.rs # Action planning and execution
│ ├── stow.rs # Stow operation logic
│ ├── unstow.rs # Unstow operation logic
│ ├── adopt.rs # Adopt/override functionality
│ └── ignore.rs # Pattern matching
├── tests/
│ └── integration_tests.rs # Integration tests
├── docs/
│ └── TECHNICAL_CONCEPT.md # Technical documentation
├── Cargo.toml
├── README.md
├── LICENSE
├── CHANGELOG.md
└── DISTRIBUTION.md
Testing
All tests use temporary directories and should be safe to run:
# All tests (unit + integration)
cargo test
# Just integration tests
cargo test --test integration_tests
# Test with output
cargo test -- --nocapture
# Tests requiring symlink creation (needs Developer Mode)
cargo test -- --ignored
On Windows systems that use Docker instead of a local Rust toolchain, run
bash check-windows-docker.sh from Git Bash. This builds winstow.exe and the
test executables in Docker, then runs the tests on Windows so symlink behavior
is tested on the actual host. The script expects the rust-docker skill scripts
in ~/.claude/skills/rust-docker/scripts; set RUST_DOCKER_SCRIPTS if they
are installed elsewhere. Docker Desktop and Developer Mode must be enabled.
Contributing
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
License
MIT License - see LICENSE file for details.
Acknowledgments
- Inspired by GNU Stow by Bob Glickstein
- Built with Rust
- Uses clap for CLI parsing
- Uses windows-rs for Windows API access
- Uses thiserror for error handling
- Uses serde for serialization/deserialization
- Uses toml for configuration file parsing
- Uses glob for pattern matching
- Uses dirs for platform-specific directory paths
Support