AS

asdf

Extendable version manager for multiple languages

Developer Tools & Git ★ 25.6k stars Easy setup MIT

asdf is a CLI tool that manages multiple runtime versions of languages and tools through a plugin system with a single per-project config file. It runs locally on developer machines.

Key features

  • Manage many runtimes
  • Plugin ecosystem
  • Single config file
  • Cross-platform

Pros & cons

Strengths

  • Manages many runtimes
  • Per-project version files
  • Large plugin catalog

Trade-offs

  • Shims can confuse tooling
  • Plugin quality varies

asdf replaces

Last reviewed Aug 26, 2026 · 860 words

One file, .tool-versions, committed next to the code, pins every runtime a project needs: nodejs 22.11.0, python 3.12.7, terraform 1.9.8, whatever the plugins cover. That is the entire idea of asdf, and it is why a version manager has 25,547 stars. Where nvm handles Node and pyenv handles Python and each has its own shell hook and its own way of breaking, asdf handles all of them through one plugin interface and one config file. It is MIT, ships as a single Go binary since its rewrite, and its 64 MB footprint is really the footprint of whatever it just installed. It is also the tool self-hosters most often put in the wrong place, which is what this guide is about.

It manages toolchains, not services

asdf lives on developer machines and build hosts. It is not how a Node app should find Node inside a container, and it is not a process manager. The right mental model is a per-project lock for tools, the way a lockfile is a per-project lock for libraries. In a homelab that puts it in exactly 3 places: the workstation you develop on, the box where you build things from source (a Gitea Actions runner, a build VM), and any long-lived server where you run scripts that depend on a specific interpreter. Production containers should install their runtime in the Dockerfile and never see asdf.

The commands that cover 95 percent of use

Install the binary, add the two shell lines the docs give you, then:

asdf plugin add nodejs
asdf plugin add python
asdf list all nodejs | tail -5        # what can be installed
asdf install                          # everything in ./.tool-versions
asdf set nodejs 22.11.0               # writes ./.tool-versions
asdf set --home python 3.12.7         # writes ~/.tool-versions
asdf reshim                           # after any global npm/pip install

asdf install with no arguments reads the project file and installs anything missing, which is the whole onboarding story for a new machine: clone, asdf install, done. Resolution walks up from the current directory to the nearest .tool-versions, falling back to ~/.tool-versions, so a server-wide default and per-project overrides coexist without ceremony.

Shims are the price, and they surprise people

asdf puts a directory of shims on your PATH, one tiny script per executable. Running node runs the shim, which finds the right .tool-versions, then executes the real binary. Two consequences follow. First, every invocation pays a resolution cost of tens of milliseconds, which is invisible for a shell session and noticeable when a build tool spawns the interpreter 400 times. Second, anything that resolves the binary path once and caches it, such as an editor, a systemd unit or a cron job, may pick up the shim without the environment the shim expects. For systemd and cron, use the real path from asdf which node and skip the shim entirely. For a global tool installed with npm install -g, run asdf reshim or the new command will not exist on your PATH.

Plugin quality is the real risk

The plugin catalogue is enormous, and that is both the strength and the weakness. The core plugins for Node, Python, Ruby, Erlang and Elixir are maintained by the asdf organisation and well behaved. Beyond those, a plugin is a shell script somebody wrote, and it may or may not handle a new release, a new CPU architecture or a checksum change. Two practical notes: the Python plugin builds CPython from source through python-build, so a small VPS wants build-essential, libssl-dev, libffi-dev, zlib1g-dev and about 5 minutes per version; and the Node plugin downloads prebuilt binaries, so it is quick but only for platforms Node publishes for. Read a plugin's repository before trusting it on a server.

mise reads the same file and skips the shims

If you are choosing fresh in 2026, look at mise before committing. It reads .tool-versions unchanged, so migration is a rename of nothing, but it is written in Rust, updates PATH on directory change instead of shimming, and adds task running and per-directory environment variables. Teams that standardised on asdf years ago have no urgent reason to move; the plugin ecosystem is shared, and the file format is the standard. Individuals starting now mostly end up on mise. Both belong in the same developer tools category and solve the same problem; they differ on mechanism and scope.

What I'd do

Put .tool-versions in every repository that has a runtime dependency, run asdf install in the CI runner's setup step so the build uses exactly what the developer used, and keep asdf off production hosts. On the build box, set a ~/.tool-versions for the defaults and let projects override. If a systemd unit ever needs to call an asdf-managed interpreter, hardcode the path from asdf which. And if you are setting up a new laptop today with no existing asdf muscle memory, install mise instead, point it at the same file, and lose nothing.

Compare asdf

1 head-to-head comparisons.

Similar developer tools & git apps