Tether installs your global packages on every machine. So a bad release or a changed manifest could reach all of them. Tether checks each synced package before a package manager runs.
A sync tool spreads what you install. That is its job. It also spreads a bad package to each machine that syncs.
Compromised releases.
An attacker who takes over a maintainer account can publish a malicious version to npm, PyPI or RubyGems.
Registries often remove such versions after someone reports them. A machine that installs before then gets the bad version.
Worm-style packages.
Some malicious packages run code at install time. They steal tokens from the machine and use them to publish more infected packages.
Install scripts are a common way for this code to run.
Stolen repo tokens.
Anyone with push access to your sync repo can add a line to a manifest.
Without checks, each of your machines would install that package on its next sync.
THE DEFENCES
In the manifest. Not yet approved.
Tether runs these checks on every sync. It runs them again for each held package. A package that fails a check waits in the Inbox on this machine.
FROM MANIFEST TO INSTALL
Manifest changeA package arrives from another machine
Run the checksTrusted record · Name · Tap · Release age · OSV
Install or holdInstall it, or hold it in the Inbox
Tether checks held packages again on each sync. A package installs when no check holds it.
A release-age limit.
npm, pnpm, bun and uv skip releases newer than 7 days. Set packages.min_release_age_days to change the limit. 0 turns it off.
Some managers cannot enforce the limit: gem, npm before 11.10, pnpm before 10.16 and bun before 1.3. Their synced packages wait for approval.
Install scripts off.
npm, pnpm and bun install and upgrade with install scripts turned off.
Add a package to packages.allow_scripts when it needs its scripts. Older npm and pnpm cannot limit scripts to one package, so its scripts stay off there.
Malware checks with OSV.
Tether checks npm, pnpm, bun, uv and gem packages against OSV before it installs or upgrades them. A MAL- advisory blocks that release.
Tether finds the release that would install, checks it, and installs exactly that release.
Names, not commands.
Tether skips manifest names that look like flags, URLs, paths, tarballs or git+ specs.
Every package manager runs in an empty directory, ~/.tether/run. A local file or project config cannot change what it installs.
Taps you chose.
Homebrew taps outside homebrew/* must be in packages.brew.trusted_taps. Other taps, and their formulae and casks, wait in the Inbox.
Tether finds the tap of a short name, such as bun, before it installs it. Upgrades name only packages from trusted taps.
Exact versions.
Each signed machine record lists the version it installed of each npm, pnpm, bun, uv and gem package. Manifests list only names.
A sync installs the newest version that a trusted machine lists. A manifest cannot pick an older version.
THE TRUST MODEL
Signed records. Your trust list.
Each machine has its own SSH signing key. Every sync signs the machine record that lists its packages and versions.
Your trust list stays on this machine and never syncs. A package installs on its own only when a trusted, signed record lists that exact version. A line in a manifest grants no trust.
Trust is transitive. When you trust a machine, you also trust the packages it installed. A package you rejected on this machine stays blocked.
Terminal
tether machines show
tether machines trust <id> --fingerprint SHA256:...
ON EACH SYNC
Sign the recordmachines/<id>.json · ed25519 key
Verify locallyMatch the key in ~/.tether/trusted_keys
Install listed packagesHold all other packages in the Inbox
Tether never moves trust to another machine id on its own.
RECORDS THAT CHANGE
A push is not a signature.
Someone with push access can edit or replace machine records. Tether checks each record before it counts.
Old records do not count.
Each record has a generation that increases with every save. Tether keeps the newest generation it accepted from each key.
An older signed record, such as one from git history, grants no trust.
Changed keys stop.
When a trusted machine signs with a different key, Tether warns you. It does not trust that record until you approve the new key.
Tether ignores a record that fails its signature, and shows a warning.
Your record, from your copy.
This machine builds its record from ~/.tether/machine.json, not from the repo.
A change that someone pushes to the repo copy does not get your signature.
YOU DECIDE
One Inbox. Every held item.
Held packages, taps and machine keys wait in the Inbox, ~/.tether/inbox.json. The Inbox never syncs. Review it in the terminal or on the dashboard Security tab.
What the Inbox holds.
Packages that no trusted machine record lists
Packages from untrusted Homebrew taps
Packages whose manager cannot enforce the release-age limit
Releases with a MAL- advisory, which you cannot approve
A release for this OS, when the version that another OS pins fails here
New machine keys and changed machine keys
How you decide.
An approval covers one version, one Homebrew tap, or one machine key. A rejected item is not offered again.
Tether refuses a decision when the item changed after you saw it. It then shows you the new item.