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).
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.
I'm still learning Rust, so the code may not be the most idiomatic or efficient. Suggestions for improvement are welcome.
- ✅ 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:
--adoptand--overridefor handling existing files - ✅ Pattern Matching:
--ignoreand--deferfor flexible file management - ✅ Configuration Files: Set defaults with
.winstowrc - ✅ Small & Fast: Single ~600KB binary
- ✅ Safe: Dry-run mode to preview changes before applying
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
Download the latest release from GitHub Releases and add it to your PATH.
# 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# WinGet
winget install winstow# 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# 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
| Flag | Long Form | Description |
|---|---|---|
-S (optional) |
--stow |
Stow packages (default action) |
-D |
--delete |
Unstow (delete) packages |
-R |
--restow |
Restow packages (unstow then stow) |
| 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 (stow/restow only, destructive) | |
--ignore PATTERN |
Skip files matching pattern (stow/restow only) | |
--defer PATTERN |
Skip files matching pattern if they already exist in target (stow/restow only) | |
-h |
--help |
Show help message |
-V |
--version |
Show version |
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 packagesGit 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# If files already exist in target, you'll get an error
winstow mypackage
# Error: Conflict: C:\Users\You\.gitconfig already exists
# 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 --override mypackage
# Removes .gitconfig, then creates symlink (destructive!)# 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-bDifference 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)
PowerShell:
# After updating your dotfiles repository
cd $env:USERPROFILE\Dotfiles
git pull
# Refresh symlinks
winstow -R -d $env:USERPROFILE\Dotfiles -t $env:USERPROFILE GitGit Bash:
# After updating your dotfiles repository
cd $USERPROFILE/Dotfiles
git pull
# Refresh symlinks
winstow -R -d $USERPROFILE/Dotfiles -t $USERPROFILE GitPowerShell:
# 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)Create a .winstowrc file in one of these locations:
- Current directory:
./.winstowrc - Home directory:
%USERPROFILE%\.winstowrc - AppData:
%APPDATA%\winstow\config.toml
# 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 = falseCLI arguments always override config file settings.
winstow implements directory folding (inspired by GNU Stow) for efficiency:
# 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
# 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
- Always use
-nfirst to preview changes before applying them - Use
-vfor debugging to see detailed operations and understand what's happening - Set up
.winstowrcfor 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!
Problem: Cannot create symbolic links
Solutions:
- Enable Developer Mode (Settings → For developers)
- Run as Administrator
- Check Windows version (requires Windows Vista+)
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
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 mypackageProblem: 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 build
cargo build
# Release build (optimized)
cargo build --release
# Run tests
cargo test
# Run tests including ignored ones (requires Developer Mode)
cargo test -- --ignoredwinstow/
├── 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
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 -- --ignoredContributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
MIT License - see LICENSE file for details.
- 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