Skip to content

Latest commit

Β 

History

154 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸͺ’ Knot

A lightweight, configurable dotfiles manager.

Go Report Card CI Release License: MIT

Knot is a CLI tool for managing dotfiles via symlinks. Like GNU Stow, it centralizes your configuration files in a single repository. Unlike Stow, Knot is fully configurable β€” you explicitly define where files go, ignore specific files, and apply OS-specific rules without a rigid directory structure.

✨ Features

  • Intuitive CLI: Simple commands like knot tie and knot untie
  • Configurable Routing: Map any file in your dotfiles repo to any location on your system
  • Ignore Rules: Exclude specific files (e.g. README.md, .DS_Store) per package
  • OS Conditions: Conditionally tie packages based on the operating system (macOS vs Linux)
  • Tags: Group packages with tags: [work, linux] and bulk-operate with --tag <name>
  • Safe by Default: knot plan previews every change before anything is written
  • Validation: knot validate checks your Knotfile for errors before you run anything
  • Interactive TUI: Run knot with no arguments for a live package/tags dashboard
  • App Install: Declare install metadata per package to check versions and install apps from the TUI
  • File Templating: .tmpl files are rendered with Go text/template using runtime variables like .os, .hostname, .home

πŸš€ Installation

Homebrew (recommended)

brew install oxGrad/tap/knot

Installs a pre-built binary for macOS (Intel + Apple Silicon) and Linux via the oxGrad/homebrew-tap tap.

Download a release binary

Pre-built binaries for Linux, macOS, and Windows are available on the Releases page.

# Example for Linux amd64
curl -L https://github.com/oxGrad/knot/releases/latest/download/knot_linux_amd64.tar.gz | tar xz
sudo mv knot /usr/local/bin/

Go install

go install github.com/oxgrad/knot@latest

βš™οΈ Configuration (Knotfile)

Create a file named exactly Knotfile (no extension) at the root of your dotfiles repository. Knot searches upward from the current directory to find it automatically.

packages:
  # source defaults to ./nvim when omitted
  nvim:
    target: ~/.config/nvim
    tags: [work]
    ignore:
      - "README.md"
      - ".DS_Store"
    install:
      bin: nvim
      brew: neovim
      apt: neovim
      dnf: neovim

  # Map zsh files directly into the home directory
  zsh:
    source: ./zsh
    tags: [home]

  # OS-specific package β€” only tied on macOS; belongs to two tags
  yabai:
    target: ~/.config/yabai
    tags: [home, macos]
    condition:
      os: darwin

  # Untagged β€” still usable by name
  secrets:
    target: ~/.ssh

Knotfile fields

Field Required Description
source β€” Path to source directory (relative to Knotfile, or absolute; ~ supported). Defaults to ./<package-name>
target βœ… Destination path for symlinks (~ supported). See linking modes below.
ignore β€” List of glob patterns matched against file basenames
tags β€” List of tag names; enables --tag flag and Tags tab in TUI
condition.os β€” Only tie on this OS (darwin, linux, windows, freebsd)
install β€” Optional app-install metadata β€” see App Install below

πŸ“₯ App Install (install:)

Adding an install: block to a package enables version checking and, in the TUI, an i key that installs the app itself.

packages:
  nvim:
    target: ~/.config/nvim
    install:
      bin: nvim
      deps: [git]

  secrets:
    target: ~/.ssh
    install:
      bin: age
      script: curl -fsSL https://age-encryption.org/install.sh | sh
      autoResolve: false
Field Required Description
install.bin β€” Binary name checked with which and --version to detect an existing install (e.g. nvim)
install.brew β€” Homebrew formula name
install.apt β€” apt-get package name
install.dnf β€” dnf package name
install.script β€” URL for a curl install script, piped to bash
install.deps β€” Other knot package names to install first, via the same package manager
install.autoResolve β€” Fill empty brew/apt/dnf with the package name. Defaults to true

bin β€” the binary knot looks for on $PATH to decide whether the package is already installed, and to read its version for the Packages tab. If omitted, it falls back to the package's Knotfile name.

brew / apt / dnf β€” the package name passed to each manager's install command. Leave these unset and let autoResolve fill them in when the name matches the package's Knotfile key (the common case); set one explicitly when a manager's package name differs (e.g. apt: neovim where the Knotfile package is named nvim).

Manager Binary checked OS
brew brew macOS, Linux (Homebrew on Linux)
apt apt-get Linux (Debian/Ubuntu)
dnf dnf Linux (Fedora/RHEL)

Pressing i in the TUI only lists managers configured on the package and present on $PATH β€” e.g. on Fedora, apt never shows up even if install.apt resolves to a name, since apt-get isn't installed.

script β€” a shell one-liner (typically a curl | sh) run when there's no package-manager entry to use, or as an option alongside them. Checked against curl on $PATH.

deps β€” other package names (from this same Knotfile) to install first, using the same manager the user picks for this package. Useful when an app needs a build dependency (e.g. tmux depending on deps: [git]).

autoResolve β€” when true (the default), any of brew/apt/dnf left empty are filled in with the package's Knotfile name, so a package whose name matches across all three managers needs no repetition. Set it to false to opt a package out entirely β€” e.g. a script-only install like secrets above, where "secrets" isn't a real package in any manager.

Linking modes

The target value controls how knot places symlinks:

Directory symlink (default) β€” knot creates a single symlink at target pointing to the entire source directory. Use this when the target path does not yet exist and you want the whole directory to be managed as one unit.

nvim:
  target: ~/.config/nvim   # creates ~/.config/nvim -> /dotfiles/nvim

Per-file mode β€” add a trailing / to target. Knot links each file in the source directory individually into target. This is required when target is a directory that must already exist (like ~/ or ~/.config/), and also respects ignore patterns.

zsh:
  target: ~/               # links ~/dotfiles/zsh/.zshrc -> ~/.zshrc, etc.
  ignore:
    - "README.md"

File templating

In per-file mode, any source file ending with .tmpl is rendered with Go text/template before being linked. The .tmpl suffix is stripped in the target name (e.g. config.tmpl β†’ config).

Available template variables:

Variable Description
.os Runtime OS (darwin, linux, windows, …)
.arch CPU architecture (amd64, arm64, …)
.hostname Machine hostname
.username Current user name
.home Home directory path
.env Map of all environment variables

Example β€” write a config that adapts to the current OS:

# zsh/.zshrc.tmpl
{{ if eq .os "darwin" }}
eval "$(/opt/homebrew/bin/brew shellenv)"
{{ end }}
export PATH="$HOME/.local/bin:$PATH"

A JSON Schema is available for editor validation and auto-complete β€” see Editor Integration.

πŸ› οΈ CLI Reference

knot tie [package...] [--all] [--tag <name>]   Create symlinks
knot untie [package...] [--all] [--tag <name>]  Remove symlinks
knot status                                       Show symlink state
knot plan [package...] [--all] [--tag <name>]   Dry-run preview
knot validate                                     Validate Knotfile
knot init [git-url]                               Create or clone a Knotfile
knot version                                      Print the knot version

Global flags available on every command:

--config string   Path to Knotfile (default: auto-discover upward from cwd)
--dry-run         Print actions without executing them

knot tie

Creates symlinks for the specified packages. Skips packages that are already correctly linked. Warns on conflicts (target exists but is not the expected symlink) without overwriting.

knot tie nvim zsh        # tie specific packages
knot tie --all           # tie every package in the Knotfile
knot tie --tag work      # tie all packages tagged "work"
knot tie nvim --dry-run  # preview without writing

knot untie

Removes symlinks previously created by knot tie.

knot untie nvim
knot untie --tag home    # untie all packages tagged "home"

knot status

Shows the current state of every managed symlink:

[OK]       ~/.config/nvim
[MISSING]  ~/.zshrc
[CONFLICT] ~/.config/karabiner: target exists and is not a symlink

knot plan

Dry-run that shows exactly what tie would do:

  + ~/.config/nvim -> /dotfiles/nvim
  = ~/.zshrc (already linked)

Plan: 1 to create, 0 to remove, 1 already linked, 0 conflicts

knot init

Creates a starter Knotfile in the current directory, or clones an existing dotfiles repository:

knot init              # scaffold a Knotfile in the current directory
knot init <git-url>   # clone a dotfiles repo and set it up

knot version

Prints the current knot version and exits.

knot validate

Validates the Knotfile without touching the filesystem:

knot validate
# Validating Knotfile: /home/user/dotfiles/Knotfile
#
#   ERROR [yabai]: source directory "/home/user/dotfiles/yabai" does not exist
#
# Validation failed: 1 error(s), 0 warning(s)

Exit codes: 0 = valid Β· 1 = errors Β· 2 = warnings only

Interactive TUI

Run knot with no arguments to launch the interactive TUI. It shows a live view of all packages and lets you toggle, apply, and reload without typing individual commands.

The TUI has two tabs: Packages (the default) and Tags. Switch between them with [ and ]. The Tags tab shows packages grouped by tag in a collapsible tree view β€” press enter to collapse or expand a tag, and space to bulk-toggle all packages in a tag.

Key bindings:

Key Action
↑/↓ or j/k Navigate
space Toggle package / bulk-toggle tag
enter Collapse/expand tag (Tags tab)
[ / ] Switch tabs
a Apply pending changes
i Install selected package (Packages tab)
r git pull and reload
b Switch branch
e Open dotfiles dir in $EDITOR
m Cycle mascot character
q Quit

πŸ–₯️ Editor Integration

Knot ships a JSON Schema for the Knotfile format, enabling inline validation, hover documentation, and auto-completions in any editor that supports yaml-language-server.

Schema URL:

https://raw.githubusercontent.com/oxGrad/knot/main/schema/knotfile.schema.json

Inline modeline (any editor)

Add this comment as the first line of any Knotfile. yaml-language-server picks it up automatically regardless of which editor you use β€” no editor configuration required.

# yaml-language-server: $schema=https://raw.githubusercontent.com/oxGrad/knot/main/schema/knotfile.schema.json
packages:
  nvim:
    source: ./nvim
    target: ~/.config/nvim

VS Code

Copy into your workspace .vscode/settings.json. Requires the YAML extension by Red Hat.

{
  "yaml.schemas": {
    "https://raw.githubusercontent.com/oxGrad/knot/main/schema/knotfile.schema.json": "**/Knotfile"
  },
  "yaml.validate": true,
  "yaml.completion": true,
  "yaml.hover": true
}

nvim-lspconfig

Add the schema to your yamlls setup:

require("lspconfig").yamlls.setup({
  settings = {
    yaml = {
      schemas = {
        ["https://raw.githubusercontent.com/oxGrad/knot/main/schema/knotfile.schema.json"] = "**/Knotfile",
      },
    },
  },
})

Global yamlls config (Helix, Zed, and others)

Add the schema to whichever config file your editor reads for yaml-language-server:

{
  "schemas": {
    "https://raw.githubusercontent.com/oxGrad/knot/main/schema/knotfile.schema.json": ["**/Knotfile", "Knotfile"]
  }
}

Neovim plugin

A full Neovim plugin lives in editors/neovim/. It provides:

  • Filetype detection for files named Knotfile
  • YAML syntax highlighting with Knotfile-specific keyword groups
  • Treesitter YAML parser override (Neovim 0.9+)
  • πŸͺ’ devicon registration for nvim-web-devicons
  • Automatic yaml-language-server schema configuration at runtime (no manual lspconfig setup needed)

See editors/neovim/README.md for installation instructions (lazy.nvim, packer.nvim, and manual).

πŸ“¦ Releasing a new version

Releases are automated via GoReleaser. Push a semver tag to main:

git tag v1.2.3
git push origin v1.2.3

This triggers the release workflow which:

  1. Builds binaries for Linux, macOS (Intel + Apple Silicon), and Windows
  2. Creates a GitHub Release with archives and checksums.txt
  3. Pushes an updated knot.rb formula to oxGrad/homebrew-tap

Prerequisite: A HOMEBREW_TAP_GITHUB_TOKEN repository secret must be set β€” a GitHub PAT with contents: write permission on the oxGrad/homebrew-tap repository.

🀝 Contributing

Pull requests are welcome. The CI pipeline runs on every PR to main:

Check Tool
Tests go test ./...
Build go build ./...
Lint golangci-lint

All three checks must pass before a PR can be merged.

License

MIT

About

πŸͺ’ A modern, Go-based alternative to GNU Stow with explicit mapping, ignore rules, and editor integration.

Topics

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages