Commands

On this page
CommandResolves versionsWrites block.lockDownloadsInstallsRuns your command
block lock [tool...]yesyesartifacts with no upstream digestnono
block lock --checkyesnevernevernono
block syncneverneverlocked URLs, when not cachedyesno
block exec <cmd>neverneverneverneveryes
block which <cmd>neverneverneverneverno
block status [--json]neverneverneverneverno
block list [ecosystem]neverneverneverneverno
block explain <code>neverneverneverneverno
block completion <shell>neverneverneverneverno

“never” is a guarantee, not a default: there is no flag that turns any of those cells into a yes.

block lock #

Resolves every tool in block.toml to the newest upstream release its constraint allows, and records the download URL and SHA-256 per platform. Naming tools re-resolves only those and keeps the other pins:

$ block lock              # every tool
$ block lock hermes       # only hermes

It is the only command that moves a pin.

A constraint is a dotted version prefix ("1", "1.7", "1.7.4"), one pre-release named exactly ("1.8.0-rc1"), or a channel — a release line an upstream publishes under a tag that moves:

[tools]
foundry = "nightly"

A channel is the one input that floats, and block.lock never records it as one. lock dereferences the moving tag and pins the release published for the commit under it, whose tag never moves again:

$ block lock
foundry  locked nightly-5e88010a83d1b87b8f4d13058e42a2949d3e9dc0

From there a nightly is a pin like any other: sync installs that release and nothing else, and the pin moves when — and only when — lock is run again. Which channels a tool has is the upstream’s business and the recipe’s; asking for one it does not publish names the ones it does.

One release of a channel can also be named outright, by the tag the upstream published it under:

[tools]
foundry = "nightly-e469863b1ac3f2d9d48f9d25d068a14861060cb3"

That is the same channel with the floating taken out. nightly is resolved through whatever the moving tag points at today, so it follows the upstream’s retagging — and stops following it when the upstream stops retagging, which is what has happened to Foundry’s nightly: the daily builds still arrive as nightly-<commit> tags, and the moving tag no longer catches up with them. Naming the tag says which build you mean and leaves nothing for an upstream to move. block lock resolves it in one request and writes the same pin back every time it is run.

Errors first, since a named release has two ways to be wrong: a commit the upstream does not publish is BLK2003, and a release line the recipe does not declare is BLK2009 — the same refusal foundry = "canary" earns, listing the channels there are.

block lock --check #

The same resolution, writing nothing and downloading nothing. It compares the whole prospective lockfile, so a change that leaves the version alone — a narrowed constraint, a renamed executable, a moved artifact — is reported too:

$ block lock --check
foundry  1.7.1 -> 1.7.2
hermes   1.13.3 (up-to-date)
$ echo $?
2
ExitMeaning
0block.lock is current
2block.lock would change
1error

block sync #

Installs every artifact block.lock names for this machine. It needs the lockfile’s exact URLs and nothing else — no registry, no GitHub API — and fails, without resolving or writing anything, when the lockfile is missing, disagrees with block.toml, lacks this platform, or an artifact’s checksum does not match.

Several tools are downloaded and installed at once — a few at a time, bounded by a limit block manages internally; it is not a flag or a setting. The report is still one line per tool, in lockfile order, printed once every tool has finished:

$ block sync
foundry  1.7.4   installed
hermes   1.13.0  installed
commands: anvil, cast, chisel, forge, hermes

The last line is what this project’s toolchain now provides — the commands block.lock names, whether they were installed just now or were already there. It is about the project, not the machine: the shim directory those commands live in is shared with every other project you have synced, and --verbose is what prints that wider view:

$ block sync --verbose
foundry  1.7.4   installed
hermes   1.13.0  installed
commands: anvil, cast, chisel, forge, hermes
shim directory: /home/me/.local/share/block/shims
shims written: anvil, cast, chisel, forge, hermes
shims present: anvil, cast, chisel, forge, gaiad, geth, hermes

shims present is every command the directory serves, including the ones other projects put there. Nothing is ever removed from it.

Nothing about a single install changes because others run beside it: every artifact is still verified against the locked SHA-256, served from the content-addressed cache when it is already there, and moved into the store with one atomic rename. The first failure stops the rest and is the error reported, so a sync that fails fails for one named reason.

block exec #

Runs a command with every executable from block.lock first on PATH, and exits with the command’s status:

$ block exec forge test
$ block exec make test
$ block exec ./scripts/integration-test.sh

It never downloads, installs or resolves — but it does check, offline, that the toolchain is the one block.toml asks for and that the install is intact. SIGINT and SIGTERM reach the child, so a node or a local test network shuts down the way it would outside block.

block which #

Prints the absolute path of the executable block exec would run for a command — the one the project’s installed toolchain provides:

$ block which solc
/home/me/.local/share/block/tools/solc/0.8.30-1a2b3c4d5e6f/solc

Unlike the shell’s which, it never consults PATH. The answer comes from block.toml, block.lock and the store, so a same-named binary installed some other way does not change it. From a nested directory it resolves to the enclosing project, exactly as exec does.

It fails as exec would, with the same codes, when the project is not ready: a missing lockfile (BLK1003), a lockfile that no longer matches block.toml (BLK1005), a tool that is locked but not yet installed (BLK4004). For a command no locked tool provides it deliberately differs: exec falls through to PATH for such a command, which refuses it (BLK5001), because the question it answers is what block runs — and for that command, block runs nothing:

$ block which forge
block: BLK4004: foundry 1.7.4 is not installed; run "block sync"

$ block which node
block: BLK5001: block.toml does not lock a tool providing the command "node"

One line on stdout, nothing else, so a script can take it as it is:

$ SOLC="$(block which solc)" && "$SOLC" --version

which never downloads, installs or resolves anything.

block status #

Reports what block.toml, block.lock and the store say about each tool, and changes none of them — no resolution, no network, no download, no install, no shims, no lockfile:

$ block status
TOOL      WANTED   LOCKED   INSTALLED   STATE
foundry   1.7      1.7.4    1.7.4       ok
hermes    1.13     1.13.0   -           missing

run "block sync" to install the locked toolchain
StateWhat it meansWhat fixes it
okinstalled, and the pin is what block.toml asks fornothing
missingpinned, not installed for this machineblock sync
damagedinstalled, but the marker or an executable is goneblock sync
staleblock.lock does not match block.toml — not locked yet, a changed constraint or source, no artifact for this machine, or a pin nobody declares any moreblock lock
ExitMeaning
0every tool is ok
2something needs doing
1error — no block.toml, or a block.lock that cannot be read

--json prints the same report as one object, for CI and other tools:

$ block status --json | jq -r '.tools[] | select(.state != "ok") | .name'
hermes

Every key is always present, so a consumer can read .ready for the whole answer and .tools[].state for each tool without checking whether a field exists.

Shims: the tools by their own names #

block sync also puts one small file per command in $BLOCK_HOME/shims. Add that directory to PATH once — by hand; block never edits your startup files — and the commands are just commands:

$ cd defi && forge --version
forge Version: 1.5.1-v1.5.1

$ cd ../bridge && forge --version
forge Version: 1.7.1

Each shim is the block binary under another name. Run as forge, it finds the block.toml above the working directory, reads that project’s block.lock, and runs the version pinned there. Nothing is stored in the shim, so there is one forge for every project, nothing to regenerate when you switch branches, and no shell hook or eval anywhere.

A shim does what block exec does and no more — it never resolves, downloads, installs or writes:

$ forge test
block: BLK4004: foundry 1.7.1 is not installed; run "block sync"

Outside a project, or for a command the current project does not lock, it runs the next command of that name on PATH instead, so the directory being on PATH cannot take a tool away from the rest of your system.

CI should keep using block exec: it needs no PATH setup and says what is happening.

block list #

Answers what can block install?, and with an argument, which tools exist for this blockchain system? It reads the registry snapshot embedded in the binary: no network, no block.toml, no block.lock.

That snapshot is a vendored copy of block-registry at one revision, built into the binary rather than fetched, so a block version always pairs with a registry it was tested against. block version prints which revision it carries.

Listing is discovery, not selection. block never derives a toolchain from an ecosystem — “Ethereum” means contract development to one repository and validator operation to another. You pick, block.toml records.

block explain #

Every refusal block prints carries a code — BLK1003, BLK3002 — and this is how to read one without a browser:

$ block explain BLK1003
BLK1003  block.lock not found

The project declares a toolchain but has never resolved it, so there is
nothing to install or run. block will not resolve one on the spot: which
version a build uses is a decision that gets committed, not one made by
whoever happened to run the command.

Fix
Run "block lock" to resolve block.toml into block.lock, and commit both
files.

Exits 1. Since v0.1.0.
https://nao1215.github.io/block/errors/#blk1003--blocklock-not-found

The prefix is optional and the case does not matter, because neither survives being retyped from a terminal: block explain blk1003 and block explain 1003 find the same entry. Like list, it is offline and read-only.

The whole set is at Error codes, generated from the same registry this command reads, so a code cannot mean one thing in a terminal and another in a browser.

Shell completion #

block completion prints the script that lets a shell complete block’s commands and flags. Load it once from your shell’s startup file:

$ echo 'source <(block completion bash)' >> ~/.bashrc
$ echo 'source <(block completion zsh)' >> ~/.zshrc

On zsh, a file in fpath works as well, so long as compinit runs after it:

$ block completion zsh > "${fpath[1]}/_block"
$ block completion fish > ~/.config/fish/completions/block.fish

PowerShell, in $PROFILE:

block completion powershell | Out-String | Invoke-Expression

bash can also take the script from its completion directory instead of ~/.bashrc:

$ block completion bash > /etc/bash_completion.d/block

Completion is dynamic where it can be. Inside a project, block lock <TAB> offers the tools of block.toml and block which <TAB> the commands of block.lock; anywhere, block list <TAB> offers the ecosystems and block explain <TAB> the error codes built into the binary. Completing reads those files and that table and does nothing else: it never resolves, downloads or installs.

block version #

Prints the version of block, and the block-registry revision whose recipes are compiled into it — so a resolution can be traced back to a reviewed recipe. block --version is the same answer:

$ block version
block v0.1.0
registry df8fa5946e00 (51 recipes from https://github.com/nao1215/block-registry)