tupferl
Tupferl is Austrian for "little dot" — as in das i-Tüpferl, the finishing touch.
Your dotfiles in one git repository, shared between computers. Edit them on
whichever machine you are sitting at, run tupferl sync, and it merges — asking
only about what it genuinely cannot decide.
pipx install tupferl
tupferl init git@github.com:me/dotfiles.git
tupferl add ~/.bashrc ~/.gitconfig ~/.config/nvim
tupferl sync
On the next computer, one line — init runs the first sync itself:
tupferl init git@github.com:me/dotfiles.git
Or uv tool install tupferl, or pip install --user tupferl. To run the
unreleased main branch instead:
pipx install git+https://github.com/martinus/tupferl.
Simple on purpose
No template language. No dot_ prefix or name mangling. No hooks, no scripts,
no secrets managers. ~/.bashrc is stored as .bashrc, and that is the whole
mapping.
Six commands. Four act — init, add, remove, sync. doctor checks that
this machine is set up. And status answers every question that only looks,
because they are one walk of your files:
tupferl status # what would change
tupferl status --all # every managed file, with its state
tupferl status --diff # the lines that differ
--diff goes through the pager git is already configured with, in git's own
order — GIT_PAGER, pager.diff, core.pager, $PAGER — so a machine set up
for delta needs nothing here, including
the usual per-command form:
[pager]
diff = "if [ -t 1 ]; then delta; else cat; fi"
The value is a shell command line, exactly as git treats it. Only when there is
a terminal to page: redirected, it is a plain unified diff with no colour of its
own, so tupferl status --diff | delta works too.
The conflict prompt's [e] reads git's editor the same way: GIT_EDITOR, then
core.editor, then $VISUAL and $EDITOR. For one run, say so on the command
line — GIT_EDITOR=meld tupferl sync.
Copies, not symlinks. The repository holds a copy of each file. Symlinks break with programs that rewrite their config, and a copy is what makes a real 3-way merge possible.
tupferl's own settings are a dotfile like any other.
~/.config/tupferl/config.toml holds two optional keys — ignore and
max_file_size — and it is this machine's until you say otherwise:
tupferl add ~/.config/tupferl/config.toml # share it
tupferl add --host ~/.config/tupferl/config.toml # …except on this machine
Per-machine differences without templates. A file under
.tupferl/hosts/<hostname>/ replaces the shared one on that machine. Whole
files, no variables:
tupferl add --host ~/.gitconfig # this machine gets its own version
tupferl remove --host ~/.gitconfig # …and stops having one
The override is committed and pushed like everything else, so a reinstall gets it back — but only the machine it is named for reads it.
Two-way, not generated
Most dotfile managers build $HOME from a source directory: you edit the
source, then apply. tupferl goes both ways, because it remembers — per machine —
what that machine last agreed with the repository.
That merge base is why sync can tell you changed it from the other computer
changed it from both did, per file, without being told. Only the last case is
a question:
$ tupferl sync
.bashrc: 1 conflict to settle.
1 of 1, lines 12-16 of the merged file
this computer
| export EDITOR=nvim
the repository
| export EDITOR=vim
[l] keep local [r] keep remote [b] keep both
[e] edit merged file [d] show full diff [s] skip
One question per file, one keypress per answer. [b] keeps both versions with
no markers left behind; [e] opens $EDITOR on the merged file. --ours,
--theirs and --no-input answer for you.
tupferl status says what the next sync would do, without doing it.
Plaintext, by decision
What tupferl stores, it stores in plaintext and pushes to your remote. There
is no encryption and none is planned. So add refuses a short list of names
whose only job is to hold a credential — .ssh/id_*, .aws/credentials,
.netrc, .pgpass, .gnupg/*, *.pem, *.key — and says why. --anyway
overrides it. tupferl add ~/.ssh stores config, known_hosts and id_*.pub
and skips the private keys (*.pub is the half of a key pair meant to be
shared).
No dependencies
On Python 3.11 and newer tupferl imports nothing outside the standard
library. On 3.10 it installs exactly one thing — tomli, the library
tomllib was taken from — so the single dependency is a backport of a stdlib
module, and it disappears on the interpreters that ship it.
That is a supply chain of one package that goes to zero, and it is asserted
rather than intended: tests/test_packaging.py reads every import in the
package and every requirement in pyproject.toml and refuses to let the two
disagree in either direction.
Requirements
- Python 3.10+ (3.10 gets the
tomlibackport installed for you). - git 2.25+ on
PATH.tupferl doctorchecks it and names what it found. - Linux or macOS. Windows is out of scope for version 1.
Development
pip install -e '.[dev]'
ruff check . && ruff format --check . && mypy tupferl tests tools \
&& python -m tools.run_tests # the preflight, exactly what CI runs
docs/plan.md is what this was built from, including what it
deliberately does not do. tools/ holds the test infrastructure — a runner that
refuses to call a partial run green, and a mutation harness that checks whether
a test would notice its subject breaking; see tools/README.md.
Licence
Apache-2.0. See LICENSE.
Release files for tupferl 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| tupferl-1.0.0.tar.gz | 524.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tupferl-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 621.4 kB
Release files / tupferl-1.0.0.tar.gz
| Download URL | tupferl-1.0.0.tar.gz |
|---|---|
| Size | 524.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
572f7b2727a0e23952150ef07c21abecc20861cde2d235e35c82c1f018d843dd
|
|
BLAKE2b-256 checksum How to use checksums |
2587f2989a4a9aadd95834ac78b98e0395beb69ba5e0a48f3c457e5edea5cc0d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Aug 28, 2026.
Transparency logRelease files / tupferl-1.0.0-py3-none-any.whl
| Download URL | tupferl-1.0.0-py3-none-any.whl |
|---|---|
| Size | 96.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
12c3bce9c78ff8c8042921cd54604ab37119b0c299f4d2993226db36ad3fd1f4
|
|
BLAKE2b-256 checksum How to use checksums |
fa1838bc3b26844ed076d3ea50e552e1646f8423b141b90d6d0bb349d8a5aa10
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Aug 28, 2026.
Transparency log