permachine
v0.9.2
Published
Automatically merge machine-specific config files with base configs using git hooks
Maintainers
Readme
permachine
Per-machine config management with git for tools that don't support it natively. Automatically merge machine-specific configurations with a base config.
Problem
When syncing dotfiles across multiple machines, you often need:
- Shared configuration - Settings that work across all machines
- Machine-specific overrides - Local paths, API keys, ports, etc.
- Automatic merging - No manual copy-paste or merge steps
Solution
permachine automatically:
- Detects your machine name
- Finds machine-specific config files (e.g.
config.my-laptop.json,config.workstation.json) - Merges them with base configs (e.g.,
config.base.json) - Outputs the final config (e.g.,
config.json) - Manages .gitignore - Adds output files and removes from git tracking
- Runs automatically on git operations via hooks
Quick Start
# Install globally
npm install -g permachine
# In your repository
cd /path/to/your/repo
# Initialize (one-time setup)
permachine init
# That's it! Your configs will now auto-merge on git operations when a file ends with `.<machine-name>.<ext>`CLI Reference
permachine - Automatically merge machine-specific config files
USAGE:
permachine <command> [options]
COMMANDS:
init Initialize permachine in current repository
merge Manually trigger merge operation
info Show information about current setup
uninstall Uninstall git hooks
watch Watch for file changes and auto-merge
OPTIONS:
--help, -h Show this help message
--version, -v Show version number
--silent, -s Suppress all output except errors (for merge command)
--legacy Use legacy .git/hooks wrapping (for init command)
--auto Auto-detect best installation method (for init command)
--no-gitignore Don't manage .gitignore or git tracking (for init/merge commands)
--debounce <ms> Debounce delay in milliseconds (for watch command, default: 300)
--verbose Show detailed file change events (for watch command)
EXAMPLES:
permachine init
permachine merge --silent
permachine info
permachine uninstall
permachine watch
permachine watch --debounce 500 --verboseUsage
File Naming Convention
New Advanced Syntax (Recommended):
Filter-based syntax for precise control:
| Purpose | Filename | In Git? |
| --------------------- | ----------------------------- | ------------------ |
| Base config (shared) | config.base.json | ✅ Yes |
| OS-specific | config.{os=windows}.json | ✅ Yes |
| Machine-specific | config.{machine=laptop}.json| ✅ Yes |
| Multi-filter | secrets.{machine=laptop}{user=josxa}.json | ✅ Yes |
| Final output (merged) | config.json | ❌ No (gitignored) |
Legacy Syntax (Still Supported):
Given machine name my-laptop (auto-detected from hostname):
| Purpose | Filename | In Git? |
| --------------------- | ----------------------- | ------------------ |
| Base config (shared) | config.base.json | ✅ Yes |
| Machine-specific | config.my-laptop.json | ✅ Yes |
| Final output (merged) | config.json | ❌ No (gitignored) |
Supported Filters:
{os=windows},{os=macos},{os=linux}- Operating system{arch=x64},{arch=arm64}- CPU architecture{machine=hostname}- Machine/hostname (same as legacy){user=username}- Username{env=prod},{env=dev}- Environment (from NODE_ENV)- Multiple filters:
{os=windows}{arch=x64}(AND logic) - OR logic:
{os=windows,macos,linux}(comma-separated)
📚 See File Filters Documentation for complete guide and examples.
Same pattern works for .env files:
| Purpose | Filename | In Git? |
| ---------------- | -------------------------- | ------------------ |
| Base config | .env.base | ✅ Yes |
| Machine-specific | .env.{machine=laptop} | ✅ Yes |
| Final output | .env | ❌ No (gitignored) |
Directory Matching (NEW)
In addition to file-level merging, you can apply filters to entire directories. When a directory matches, all its contents are copied as-is to the output directory (without further filter processing or merging).
Example:
.opencode/skills/
├── jira.{machine=homezone}/ # Only exists on machine "homezone"
│ ├── skill.md
│ └── templates/
│ └── issue.md
├── work-tools.{machine=laptop}/ # Only exists on machine "laptop"
│ └── slack.md
└── shared-skill/ # Regular directory (always present)
└── common.mdOn machine homezone:
.opencode/skills/
├── jira/ # ← Copied from jira.{machine=homezone}/
│ ├── skill.md
│ └── templates/
│ └── issue.md
└── shared-skill/
└── common.mdSupported filters on directories:
{machine=hostname}- Machine-specific directories{os=windows},{os=macos},{os=linux}- OS-specific directories{user=username}- User-specific directories- Multiple filters:
mydir.{machine=laptop}{os=windows}/(AND logic)
Key behaviors:
- Files inside matched directories are copied verbatim (no recursive filter processing)
- No base directory fallback (unlike files, there's no
mydir.base/pattern) - Nested filtered directories are not allowed:
outer.{machine=X}/inner.{os=Y}/→ Error - If multiple directories would produce the same output, an error is raised
- Stale outputs are renamed with
.permachine-deletedsuffix for safety
Basic Commands
Initialize in Repository
permachine initWhat it does:
- Detects your machine name (e.g.,
laptop,desktop,workstation) - Installs git hooks for automatic merging
- Scans for existing machine-specific files
- Prompts for confirmation if existing files will be overwritten
- Performs initial merge
- Adds output files to
.gitignoreand removes them from git tracking
Example output:
✓ Machine detected: laptop
✓ Git hooks installed via core.hooksPath
✓ Merged 2 file(s)
✓ Added 2 file(s) to .gitignore
✓ Removed 1 file(s) from git tracking
Git hooks will auto-merge on:
- checkout (switching branches)
- merge (git pull/merge)
- commitManual Merge
permachine mergePrompts for confirmation if existing files will be overwritten. Useful for testing or running without git hooks.
Watch Mode
permachine watchWhat it does:
- Watches all base and machine-specific files for changes
- Automatically merges when you save any watched file
Check Setup
permachine infoExample output:
Machine name: laptop
Repository: /path/to/repo
Hooks method: core.hooksPath
Hooks path: .permachine/hooks
Tracked patterns: 2
- config.base.json + config.laptop.json → config.json
- .env.base + .env.laptop → .env
Output files: 2 total, 1 existing
Existing output files:
- config.jsonCookbook / Recipes
Recipe 1: VSCode Settings Per Machine
Different settings for work laptop vs home desktop:
# On work laptop (machine: "worklaptop")
.vscode/
├── settings.base.json # Shared: theme, font size
├── settings.worklaptop.json # Work paths, proxy settings
└── settings.json # ← Merged output (gitignored)
# On home desktop (machine: "desktop")
.vscode/
├── settings.base.json # Shared: theme, font size
├── settings.desktop.json # Home paths, no proxy
└── settings.json # ← Merged output (gitignored)settings.base.json:
{
"editor.fontSize": 14,
"workbench.colorTheme": "Dark+",
"terminal.integrated.shell.windows": "powershell.exe"
}settings.worklaptop.json:
{
"http.proxy": "http://proxy.company.com:8080",
"terminal.integrated.cwd": "C:/Projects/Work"
}settings.desktop.json:
{
"terminal.integrated.cwd": "C:/Code/Personal",
"git.path": "C:/Program Files/Git/bin/git.exe"
}Recipe 2: OpenCode Config (AI Assistant)
OpenCode supports machine-specific MCP servers and model preferences:
~/.config/opencode/
├── config.base.json # Shared: agents, themes, keybinds
├── config.worklaptop.json # Work: Google Sheets MCP
├── config.homezone.json # Home: Telegram MCP, local paths
└── config.json # ← Merged output (gitignored)config.base.json:
{
"$schema": "https://opencode.ai/config.json",
"theme": "nightowl-transparent",
"keybinds": {
"input_newline": "shift+return"
},
"mcp": {
"perplexity-mcp": {
"enabled": true,
"type": "local",
"command": ["uvx", "perplexity-mcp"]
}
}
}config.homezone.json:
{
"mcp": {
"telegram-mcp": {
"enabled": true,
"type": "local",
"command": ["uv", "--directory", "D:\\git\\telegram-mcp", "run", "main.py"]
},
"google-sheets": {
"enabled": true,
"environment": {
"SERVICE_ACCOUNT_PATH": "C:/Users/josch/.config/opencode/secrets/service-account.json"
}
}
}
}config.worklaptop.json:
{
"mcp": {
"telegram-mcp": {
"enabled": false
},
"google-sheets": {
"enabled": true,
"environment": {
"SERVICE_ACCOUNT_PATH": "/work/credentials/google-service-account.json",
"DRIVE_FOLDER_ID": "work-folder-id-123"
}
}
}
}Recipe 3: Package.json Scripts (Platform-Specific)
Different scripts for macOS vs Windows development:
# package.base.json
{
"name": "my-app",
"version": "1.0.0",
"scripts": {
"test": "jest",
"lint": "eslint src/"
},
"dependencies": {
"express": "^4.18.0"
}
}
# package.macos.json
{
"scripts": {
"dev": "NODE_ENV=development nodemon src/index.js",
"build": "rm -rf dist && webpack",
"open": "open http://localhost:3000"
}
}
# package.windows.json
{
"scripts": {
"dev": "set NODE_ENV=development && nodemon src/index.js",
"build": "rmdir /s /q dist && webpack",
"open": "start http://localhost:3000"
}
}
# package.json ← Merged output
# Each OS gets appropriate shell commands!Recipe 4: Git Config
Personal vs work Git settings:
# .gitconfig.base
[core]
editor = code --wait
autocrlf = true
[pull]
rebase = true
[init]
defaultBranch = main
# .gitconfig.worklaptop
[user]
name = John Doe
email = [email protected]
[url "https://"]
insteadOf = git://
[http]
proxy = http://proxy.company.com:8080
# .gitconfig.homezone
[user]
name = JohnD
email = [email protected]
[github]
user = johnd-personalRecipe 5: Multi-File Dotfiles
Complete dotfiles setup across machines:
~/.config/
├── nvim/
│ ├── init.base.vim # Shared vim config
│ ├── init.worklaptop.vim # Work-specific plugins
│ ├── init.homezone.vim # Personal plugins
│ └── init.vim # ← Merged
├── alacritty/
│ ├── alacritty.base.yml # Shared terminal config
│ ├── alacritty.macos.yml # macOS font paths
│ ├── alacritty.windows.yml # Windows font paths
│ └── alacritty.yml # ← Merged
├── opencode/
│ ├── config.base.json
│ ├── config.worklaptop.json
│ └── config.json # ← Merged
└── .env.base
.env.worklaptop
.env # ← Merged
# After `permachine init`, sync your dotfiles repo across machines!
# Each machine automatically gets the right config.Recipe 6: Advanced Filters - Cross-Platform Development
NEW: Use the advanced filter syntax for precise control:
# Project structure
project/
├── config.base.json # Shared config
├── config.{os=windows}.json # Windows-specific paths
├── config.{os=macos}.json # macOS-specific paths
├── config.{os=linux}.json # Linux-specific paths
├── secrets.{machine=work}{user=alice}.json # Alice's work secrets
├── secrets.{machine=home}{user=alice}.json # Alice's home secrets
├── build.{os=windows}{arch=x64}.json # Windows x64 build config
├── build.{os=windows}{arch=arm64}.json # Windows ARM build config
└── config.json # ← Merged (gitignored)Example use cases:
# Multiple platforms with OR logic
package.{os=windows,macos}.json # Matches Windows OR macOS
# Specific environment AND machine
secrets.{env=prod}{machine=server-us-east}.json
# User-specific on specific machine
.vscode/settings.{machine=laptop}{user=josxa}.json
# Multiple users on shared machine
preferences.{user=alice}.json
preferences.{user=bob}.jsonSee File Filters Documentation for complete guide and examples.
How It Works
permachine uses a simple process:
Machine Detection - Automatically detects your machine name from hostname (Windows:
COMPUTERNAME, Linux/Mac:hostname()) and other system properties (OS, architecture, username, environment)File Discovery - Scans your repository for files matching patterns:
- New:
{key=value}syntax (e.g.,config.{os=windows}.json,secrets.{machine=laptop}{user=josxa}.json) - Legacy:
*.{machine}.*syntax (e.g.,config.laptop.json,.env.desktop)
- New:
Filter Matching - For new syntax, evaluates filters against current system context:
- AND logic: ALL filters must match (e.g.,
{os=windows}{arch=x64}) - OR logic: ANY value in list matches (e.g.,
{os=windows,macos})
- AND logic: ALL filters must match (e.g.,
Smart Merging - Merges base and machine-specific configs:
- JSON: Deep recursive merge (machine values override base)
- ENV: Key-value merge with comment preservation
Gitignore Management - Automatically adds output files to
.gitignoreand removes already-tracked files from gitGit Hooks - Installs hooks to auto-merge on checkout, merge, and commit operations
For detailed implementation information, see CONTRIBUTING.md and File Filters Documentation.
Supported File Types
| Type | Extensions | Merge Strategy | Status |
| -------- | --------------------- | --------------------------------- | ---------------------------------------------- |
| JSON | .json | Deep recursive merge | ✅ Supported |
| JSONC | .json with comments | Deep merge + comment preservation | ✅ Supported |
| ENV | .env, .env.* | Key-value upsert | ✅ Supported |
| Markdown | .md | Append (base + machine) | 🔜 Planned |
| YAML | .yaml, .yml | Deep recursive merge | 🔜 Planned |
| TOML | .toml | Deep recursive merge | 🔜 Planned |
| Patch | .patch | Apply git-style patch to base | 💡 Proposed |
Troubleshooting
Hooks not running
Check hook installation:
permachine infoVerify git config:
git config --get core.hooksPath
# Should output: .permachine/hooksCheck hook files exist:
ls .permachine/hooks/Merge not happening
Run manually to see errors:
permachine mergeCheck machine name matches your files:
permachine info
# Verify "Machine name" matches your file patternWrong machine name detected
Machine names are auto-detected from your system hostname. To verify:
# Windows
echo %COMPUTERNAME%
# Linux/Mac
hostnameFiles must match this name (case-insensitive).
Conflicts with other git hook tools
If you use Husky or other hook managers, use legacy mode:
permachine uninstall
permachine init --legacyThis wraps existing hooks instead of replacing them.
Output file not being gitignored
By default, permachine init and permachine merge automatically add output files to .gitignore. If this isn't working:
- Check if
.gitignoreexists and contains your output files - Verify the file was removed from git tracking:
git ls-files config.json(should return nothing) - If you used
--no-gitignore, re-run without that flag
To manually fix:
echo "config.json" >> .gitignore
git rm --cached config.jsonContributing
Contributions are welcome! Please see CONTRIBUTING.md for:
- Development setup
- Architecture overview
- Testing guidelines
- Code standards
- How to submit PRs
License
MIT © JosXa
Roadmap
- [x] JSON support
- [x] ENV support
- [x] JSONC support (comments & trailing commas)
- [x] Git hooks (hooksPath & legacy)
- [x] Automatic .gitignore management
- [x] CLI interface
- [x] Comprehensive tests (81 tests)
- [x] npm package publication
- [x] Watch mode for development
- [ ] YAML support (#1)
- [ ] TOML support (#2)
- [ ] Markdown support (#3)
- [ ] Patch file support (#4)
- [ ] Custom merge strategies (#5)
- [ ] Config file for patterns (#6)
- [ ] Dry-run mode (#7)
Credits
Inspired by:
- Husky - Git hooks made easy
- The need for machine-specific configurations across development environments
