Configuration¶
All configuration lives in ~/.config/toolTamer/configs/. ToolTamer uses your machine's hostname to find the right config directory.
Directory Structure¶
~/.config/toolTamer/configs/
├── common/ # Base config — inherited by ALL hosts
│ ├── to_install.brew
│ ├── to_install.apt
│ ├── files.conf
│ └── files/
├── common_mac/ # Optional macOS-specific base
│ ├── to_install.brew
│ ├── local_install.sh
│ ├── files.conf
│ └── files/
└── myMacBook/ # Host-specific config
├── includes.conf
├── to_install.brew
├── local_install.sh
├── taps
├── files.conf
└── files/
Configuration Hierarchy¶
ToolTamer resolves configs in this order:
common/— always included for every host- Configs listed in
includes.conf— additional layers (e.g.common_mac) - Host directory — your machine's hostname
When the same file or package appears in multiple layers, the more specific layer wins: host overrides includes, includes override common.
Includes are not recursive
If an included config has its own includes.conf, it is ignored. Only the host's includes.conf is processed.
Configuration Files¶
to_install.brew / to_install.apt / to_install.pacman¶
One package name per line. Comments start with #.
ToolTamer ensures exactly these packages are installed. Packages present on the system but not in any config file will be offered for removal (dependencies are preserved).
Packages from third-party taps need their full name
List a tapped Homebrew package fully qualified —
forketyfork/tap/clawtunes, not clawtunes. The short name only
resolves on a machine where that tap has already been added, so it makes
the package uninstallable on a fresh host; brew install user/repo/formula
adds the tap by itself. Adding a package through the TUI does this
automatically, and tt --fix-taps fixes existing entries.
What counts as a dependency
A package is kept when another installed package actually requires it —
ToolTamer asks the package manager, it does not guess from the name. That
answer is cached per machine (see cache/ below), so only the first run
after installing or removing something pays for the lookup.
Note this is a different question from "was it installed automatically". A package you installed by hand can still be required by something else, and is then kept.
files.conf¶
Maps files in the files/ subdirectory to their target location relative to $HOME.
# Format: filename;target_relative_to_home
myzshrc;.zshrc
myKittyConf;.config/kitty/kitty.conf
shellScript;bin/
- If the target ends with
/, the file keeps its original name and is placed in that directory. - Comparison uses SHA256 checksums — if checksums differ, ToolTamer asks what to do.
Ignoring files inside a tracked directory (.gitignore / .ttignore)¶
A tracked directory is mirrored as a whole, but you can carve entries out
of it. ToolTamer reads .gitignore and .ttignore files anywhere inside a
tracked directory — no git repository required — and treats everything they
match as invisible:
- it is not copied into the store when the directory is added,
- it is not written to the system and not deleted from it on sync,
- it is not hashed, so changing it does not mark the entry as modified.
The matching follows real gitignore rules (pathspec.GitIgnoreSpec):
anchors (/x), **, ! negation, trailing-slash directory patterns and
nested ignore files all work. In each directory .gitignore is read first,
then .ttignore, so a later .ttignore rule wins and can re-include
something with !. Deeper directories win over shallower ones.
.gitignore and .ttignore are ordinary files: they are synced along with
everything else, so after a sync both sides normally hold the same rules.
The system directory is authoritative (like the worktree is for git): ToolTamer takes the rules from the system copy when it exists, and from the store only when there is no system copy yet.
.ttbak backups are always invisible, rules or not. When ToolTamer updates
a tracked file it first moves the old version aside to <path>.ttbak; those
backups are treated as if an ignore rule matched them — unconditionally, even
in a directory with no ignore files, and no ! rule can re-include them — so
a backup that just appeared next to a synced file never marks the entry as
modified or out of sync. They are never copied into the store, never
copied to the system, and never deleted by a sync; existing backups are left
where they are.
New files on the system
When you apply (a) or capture (u) a directory and the system side
has files the store does not know, the TUI asks per file whether to
adopt it (keep it, copy it across) or ignore it (write an
anchored /<path> line into .ttignore on both sides). Selected
(checked) rows are adopted; ignore them with the space key, n for all,
a to adopt all, Esc cancels the whole sync. Previously those files
were silently deleted (apply) or silently absorbed (capture).
The scripted sync (tt --syncFilesOnly, and the whole "Update System"
run) has no dialog — it just honours the ignore rules that are already
there. Ignore patterns are computed by one engine
(tui/core/ignore.py, exposed to the Bash mirror as
python3 -m tui.ttignore), so the TUI and the CLI always agree.
You can also ignore existing files on purpose: in the file manager,
press i on a tracked directory and pick files from the list. They
get an anchored rule on both sides and drop out of the sync — the
counterpart of s, which encrypts instead of hiding.
A .gitignore inside a tracked directory belongs to git
The store is itself a git repository, so a file literally named
.gitignore inside a tracked directory is read by the store's git as an
ignore file. One that lists itself (opencode's does) is then ignored and
never committed — other machines see it as missing and would delete
it. .ttignore is not affected and is the right place for rules. Use
tt --check-files (also run automatically before a file sync) to spot
this; the fix is to drop the self-ignore / the !/.gitignore re-include,
or to force-add the file (git add -f).
This needs pathspec. In the normal .venv setup it is installed
already. If a tracked directory carries ignore files but no usable
engine is available, ToolTamer refuses that directory's mirror rather
than mirroring without the filter — nothing gets deleted.
Git repositories¶
Update ToolTamer on every machine before you create your first repo entry.
files.confis deliberately left unchanged by this feature so that old and new versions ofttcan read the same store — but the store itself syncs between your machines, and a machine still running an olderttdoes not know what a.ttgitmarker is. It sees the marker directory as an ordinary tracked directory and mirrors it onto the system: the marker is copied in, and everything else in the target — including the repository's.gitdirectory and any uncommitted work — is deleted as "extra". That is exactly the loss this feature exists to prevent, and no version ofttcan stop it from the far side. Pull and install the current ToolTamer on every host that shares this config first; only then convert or add the first repo entry.
files.conf is unchanged for this case — the entry is still a directory
mapping like any other. What changes is what's inside the entry's
files/ directory: instead of a mirrored copy of the directory's
contents, it holds only a .ttgit marker file. ToolTamer never stores the
repository's contents and never mirrors it; it syncs the entry with git
clone / git pull instead.
configs/<host>/files.conf
nvim;.config/nvim
configs/<host>/files/nvim/.ttgit
url = git@github.com:you/nvim.git
branch = main
force = false
| Key | Required | Default | Meaning |
|---|---|---|---|
url |
yes | — | remote URL, used as origin |
branch |
no | remote HEAD | branch to check out |
force |
no | false |
true allows a hard reset to the remote |
Everything from the first # in a line is a comment, so values must not
contain # — but they may contain = (only the first = on a line
separates key from value). Key names are case-sensitive and must be
exactly url, branch, force; unknown keys are ignored. If a key
appears more than once, the last occurrence wins.
Syncing a repo entry (ToolTamer → system):
| Situation | Result |
|---|---|
| not present on the system yet | cloned |
| present, but not a git repository | the existing path is moved aside to <path>.ttbak, then cloned |
| present, clean, behind the remote | fast-forward pulled |
| present, clean, up to date | left alone |
| local commits not yet pushed | left alone — ToolTamer never pushes |
uncommitted changes or diverged history, force = false (default) |
left alone, reported |
uncommitted changes or diverged history, force = true |
hard-reset and cleaned to match origin/<branch> |
a branch other than branch is checked out |
skipped, reported — both branches are named |
origin on the system points somewhere else than url |
skipped, reported |
remote unreachable (git fetch fails) |
skipped for this run, reported; the rest of the sync continues |
.ttgit has no url |
reported as a broken entry, sync skipped |
force = true only ever authorizes that one hard-reset-and-clean step,
for a repo that is dirty or has diverged from its remote branch — it
changes nothing about the clone-vs-pull decision otherwise, and it never
makes ToolTamer push. Leave it false (the default) for any repository
you edit locally; set it only for ones you never touch by hand.
ToolTamer never checks out branch for you. If the repository is sitting
on some other branch — or on a detached HEAD — the entry is skipped and
reported as a branch mismatch, naming both the checked-out branch and the
one in the marker. That is deliberate: pulling or resetting would act on
the branch you are standing on, not the one the marker names, and with
force = true that would discard commits the marker never referred to.
Check the marker's branch out yourself, or press u to record the branch
you are actually on.
Saving a repo entry (system → ToolTamer, the u key in the file manager)
never captures file contents — it only refreshes url and branch in
the marker from the system's current git remote and checked-out branch,
and only writes the marker back when one of them actually changed.
ToolTamer does not push, does not pin commits, and does not handle submodules. A repository with uncommitted changes or a history that has diverged from the remote is skipped, not merged — you resolve it by hand, in the repository itself.
Secrets (encrypted entries)¶
The store is one git repo that every machine clones in full, so anything
committed to it is on every machine. Put ssh keys, tokens, .netrc and
similar material in secrets.conf instead of files.conf — those entries
are stored encrypted (age), and a machine can only decrypt the ones it
is entitled to.
# configs/common/secrets.conf
# stored;target[;scope] — same layout as files.conf, scope defaults to
# the config the entry lives in
id_ed25519;.ssh/id_ed25519
work_token;.config/work/token
A scope is a config name, so entitlement follows the include hierarchy:
a secret in common reaches every machine, one in a host config only that
host, one in common_mac the machines that include it. Each scope has an
age keypair; the private half is stored in the repo wrapped to the
members plus the admin key, and secret files are encrypted to the scope's
public key. Adding a machine re-wraps that one small key — the secret files
are not touched.
Requires the age binary (brew install age / apt install age).
Bootstrapping a new machine¶
No private key is ever copied between machines — only public keys travel.
# first machine, once
tt --secrets init # creates keys/<id>.key (local) and secrets/admin.pub
# prints the admin private key — keep it in a
# password manager, off the repo
# new machine: clone the config repo, then
tt --secrets init # registers this machine's public key
tt --secrets join --admin-key <recovery key> # becomes a member of its scopes
# move existing plaintext files into encrypted storage
tt --secrets migrate # dry run; --apply to encrypt
tt --secrets check # fails if a secret entry is plaintext
tt --secrets status # scopes, members, entries
join needs the admin/recovery key once (a paste from your password
manager). A machine that is not a member of a scope and holds no admin
key cannot decrypt it — that is the isolation. Remove a machine with
tt --secrets rotate <scope> (new scope key + re-encrypt its files).
How sync treats a secret¶
- TT → system: decrypt, write,
chmod 600. - system → TT: encrypt; only rewritten when the plaintext actually changed (age output is non-deterministic).
- Comparisons and diffs use the decrypted plaintext, never the ciphertext.
- In the file manager secrets show with an
Stoken and a lock;aapplies (decrypts),usaves (encrypts),sencrypts an existing plaintext entry. - Bash delegates all crypto to
python -m tui.secrets, so there is a single implementation; the encrypt/decrypt path needs only Python +age.
Private keys (keys/) are added to the store's .gitignore automatically,
like machine-id.
Files inside a tracked directory¶
A whole tracked directory does not have to be encrypted to protect one file
in it (e.g. ~/.config/opencode/opencode.json next to many non-secret
files). Such a file can be carved out:
The file is encrypted and recorded as its own secret entry, while an
anchored rule (/opencode.json) is written to .ttignore on both sides so
the directory mirror leaves it alone — the two engines never fight over it,
and no mirror code had to change. The rest of the directory keeps syncing
(and diffing) as plaintext.
In the file manager, s on a directory opens a picker of its visible files
to encrypt; tt --secrets unmark <path> decrypts one back into the
directory mirror.
includes.conf¶
A simple list of config directory names to include, one per line:
local_install.sh¶
An optional shell script executed every time tt --syncSys (or "Update System") runs. Scripts are executed in order: common → includes → host.
Use this for installations that can't be handled by the package manager (e.g. manual downloads, pip installs, font installations).
taps (macOS only)¶
A list of Homebrew taps, one per line:
These are added before packages are installed. Note that a package listed
under its fully qualified name (hashicorp/tap/terraform) does not need an
entry here — brew adds the tap on its own — but listing taps is still useful
for casks and for taps you want present regardless.
Generated files¶
Next to configs/ there is a cache/ directory holding machine-local data,
currently the reverse-dependency lookups that keep repeated runs fast. It is
regenerated automatically whenever your installed packages change.
Add cache/ to your config repo's .gitignore — it is specific to one
machine and has no business being shared.
Global Settings¶
ToolTamer's own settings are in ~/.config/toolTamer/tt.conf: