Install the tools nobody packaged.
Nothing runs at install time.
No install, postinstall, build or hook scripts. Not disabled by default — they have no representation in the code at all. toy downloads, checks the hash, unpacks, and symlinks. That is the whole install path.
$ toy install github:Derrity/tinyserve
$ tinyserve
toy.toml in your repo root, push a tag, donename = "tinyserve"
desc = "a very small static file server"
license = "MIT"
[bin]
tinyserve = "tinyserve"
[artifact]
"linux/amd64" = "tinyserve_{version}_linux_amd64.tar.gz"
"darwin/arm64" = "tinyserve_{version}_darwin_arm64.tar.gz"
No URLs, no version numbers, no forge name — so your next release needs zero edits to this file. Already using goreleaser? Don't even write it: toy init reads the asset filenames in ./dist and infers the whole thing.
Three things
-
1. No code runs during install
This blocks a class of attack, not a CVE. The Shai-Hulud worm that ate 700+ npm packages spread through
preinstall. Homebrew formulae are executable Ruby, which is why 6.0 had to bolt on Tap Trust. A package manager that can run code on install will eventually run someone else's.Where the line actually is: “nothing runs at install” is not “this package never gets to execute”. A package declaring
[bin] git = "payload"puts~/.toy/bin/giton your PATH — and toy discovers versions by shelling out togit ls-remote. Sogitis a reserved link name, refused unconditionally, not even--forcegets past it. It is the only check in the project that--forcecannot override. -
2. 8 lines of TOML, nobody to ask
No registration, no review queue, nobody to ask. Push a tag, cut a release, and anyone can run
toy install github:you/your-tool. Want to be searchable? Send one line to an index. Want a signature? Runtoy keygen. Both are optional. Mandatory signing is mandatory registration wearing a security badge. -
3. Delisting is harmless, and you can run your own index
An index entry is a pointer to a repo; the git URL is the source of truth. Removing an entry means “no longer searchable” and nothing else —
toy install github:owner/repokeeps working. Nobody can take a package away from you, including us. Fork the JSON and you have a complete parallel index.Which is why a maintainer who asks to be delisted gets delisted, no questions. When the cost is this low there is nothing worth arguing about.
What's in the index
The index is one file at /index.json. The client downloads all of it and searches locally, and toy comes configured with it as default — toy search already reads it. A config written by toy 0.1.0 has no index; add it:
toy index add default https://toy.cirraya.com/index.json
Every entry below is an inline entry. It installs at the third trust level (index-toyfile): the binary comes from the author's own release — the download URL is built from the forge template plus the tag, and the index has no say in it — but which asset gets installed and what the command is called comes from the index. The author never wrote a toy.toml and most likely doesn't know we listed them. Listing means “this installs cleanly”. It does not mean we audited the code, and it does not imply any relationship between the author and us. The client prints this level honestly at install time.
toy tells you who you're trusting
The last line of an install is one of these four. The column that matters is the one on the right.
| What the install prints | What you're trusting | What it does not prove |
|---|---|---|
✓ signed · key a3f9… · registered in index X (this is not a claim about github.com/Y themselves) |
You are trusting the ed25519 public key registered in the index. | It does not prove the repo owner signed it. The signed message is id + source + the toy.toml text. All three are public, and not one byte of it binds that key to the repo's owner. Anyone can fork the index and open a PR; copying a public key costs nothing. Someone can fork it, re-sign the same id with their own key, and point source at their own repo. What the check buys you is continuity — the key is pinned on first install and a swap is refused — not identity. On a first install, read source. Not the checkmark. |
· from github.com/Y · unsigned |
You are trusting GitHub's account system to say this repo belongs to this account, and TLS to say you reached GitHub. | No author signature, and none is needed. This is the default state for almost every package, and it is an honest state rather than a downgrade. |
⚠ packaging instructions come from the index · … |
The bytes are the author's; the packaging instructions are not. | The author never wrote a toy.toml; the index entry inlines one. What the index can change is which asset gets installed and what the command is called — not the download URL. An inlined toyfile can never reach the checkmark, because the signer is whoever submitted the index record, not the author. |
⚠ unsigned · first install, sha256 pinned (trust on first use) |
You are trusting nothing. | A later change to the bytes will be caught. But the first use in trust-on-first-use is unconditional: if what you got the first time was already malicious, toy will faithfully pin it. |
With a [sha256] block the hash is enforced; without one it is pinned on first install and a later change is refused — the model is SSH's known_hosts, deliberately not something new. --reinstall and --force are different things: the first only means “I know this version is installed, do it again” and waives no security check at all; the second means “I looked, I accept these changes”, and it prints every check it waived. The full threat model — including the column of things toy does not defend against — is in SECURITY.md. The left column above is the literal string the client prints; the other two columns are commentary, not output.
Installing toy itself
Up front: the first package manager always has to arrive some other way. toy exists so you don't have to curl | bash, and toy cannot install itself. There is no way out of that loop; anyone claiming to have solved it has moved it somewhere you're not looking. So neither option below is clean. They differ only in who you end up trusting.
Option 1: go install (easiest, if you have a Go toolchain)
go install github.com/Cirraya-LLC/toy/cmd/toy@latest
You're trusting Go's module proxy and checksum database (sum.golang.org) — a public, auditable transparency log. That's stronger than any promise we could make about ourselves, so use it if you have Go.
Option 2: download the release binary and check the hash yourself
# pick your platform: linux_amd64 / linux_arm64 / darwin_amd64 / darwin_arm64
V=0.1.0; P=linux_amd64
BASE=https://github.com/Cirraya-LLC/toy/releases/download/v$V
curl -fLO $BASE/toy_${V}_${P}.tar.gz
curl -fLO $BASE/checksums.txt
# Check the hash. This step is not optional -- skip it and you are trusting
# the download path unconditionally.
sha256sum --ignore-missing -c checksums.txt # macOS: shasum -a 256 --ignore-missing -c
tar -xzf toy_${V}_${P}.tar.gz
install -m 0755 toy ~/.local/bin/toy
checksums.txt ships from the same release as the binary, so it defends against tampering in transit, not against the publisher. That is the bootstrap paradox in its concrete form. On a Mac, download with curl as above, not a browser: a browser quarantines the file, tar carries that onto toy, and Gatekeeper refuses the ad-hoc-signed binary. If it happened anyway: xattr -d com.apple.quarantine toy.
There is no curl | bash installer here, and there will not be one. Handing your whole machine to a script nobody read is precisely the thing this project exists to argue against.
After installing
export PATH="$HOME/.toy/bin:$PATH" # add to ~/.bashrc / ~/.zshrc
toy doctor
toy comes configured with the index on this site, so toy search works right away and toy doctor checks it is reachable. If doctor says “no index configured” (a config written by toy 0.1.0), nothing is broken — it prints the line that adds it. And no index never meant no installs: toy install github:owner/repo does not go through one.
How it's put together
The index is decentralised. That's an architecture, not a pitch. It is a JSON file; the client downloads all of it and searches locally. Entries are pointers, and the source git URL is always the truth. Delete an entry and it stops being searchable, nothing more. Fork the file and you have a parallel index. (Entropic, which did sell decentralisation as the pitch — launched by npm's former CTO at JSConf EU in 2019 — was archived ten months later. So we lead with the two things above instead.)
Version discovery is git ls-remote --tags: one command, identical behaviour across every git forge, and no forge's REST API — so no forge's rate limit. Forges are a config table rather than three code paths; three lines of config adds one.
toy serve turns your local cache into a mirror. Because content is addressed by hash, a mirror does not have to be trusted — anyone can run one, it takes five minutes, and packages outlive their upstreams. But “need not be trusted” is not “need not be disclosed”: if the bytes came from a mirror, the install says so.
Deliberately absent: no building from source, no inter-package dependencies, no P2P/DHT, no Windows (for now). Each toy is a self-contained binary or a single script file, so there is no such thing as “I reviewed my direct dependencies, and their 47th transitive dependency was poisoned”.