CLI tool
kdabir/has avatar
kdabir/has

has: a bash script that reports tool versions and counts what is missing

✅ checks presence of various command line tools and their versions on the path

825 stars60 forksShellMIT

At a glance

What is it?
has is a single file shell script that checks whether a list of command line tools sits on your PATH, prints the version of each one, and exits with the number of tools it could not find. It installs with brew, a git clone, or a curl into a local bin directory, and it ships a .hasrc mechanism for pinning a project's tool requirements.
Who is it for?
Reach for has when you want a human readable answer to whether a machine has the tools a project needs, and when the exit status has to tell a script how many of them were absent. Do not reach for it as a version gate: it reports versions but the documentation shows no way to require a minimum version, and it has no way to install what is missing.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 42 days ago.
What is it written in?
Mainly Shell, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One script, no dependencies beyond bash

has is a bash script. That is the whole dependency story, and the quick start says so explicitly: install the script and there is no dependency apart from bash itself. The project is not a package with a binary, a runtime, or a configuration schema, and it does not need to be compiled or bootstrapped.

The usage is a list of command names as arguments. The script looks each one up on the PATH, reports the version it finds, and prints a line per tool.

bash
$ has node npm java git gradle

Each found tool is printed with a check mark followed by its version string, so the output reads as a short inventory of the toolchain rather than as log lines. A tool that is not installed is printed with a cross and no version.

bash
$ has node go javac

The exit status is the part that matters for scripting. If everything requested is present, has exits with 0. Otherwise the exit code is the number of commands that were not found, so a single missing tool gives 1 and the status carries the count rather than a mere failure flag.

bash
$ echo $?
1

That design is the reason to prefer this over a hand written loop in a shell script, where each check is a separate command -v invocation and the failure information is spread across the output.

The whitelist rejects commands it does not know

has only checks commands it recognises. If you ask for something outside its whitelist, the script answers with a message saying the command is not understood, and the check is not performed.

The documented escape hatch is an environment variable. Setting HAS_ALLOW_UNSAFE to the letter y makes has check an unrecognised command anyway, attempting the existence check and a best effort version detection.

The requirement on the value is exact: the documentation states that the value must be exactly y for it to work, so HAS_ALLOW_UNSAFE=yes will not do what you might expect.

This is a sensible design rather than an arbitrary one. A tool that only understands a fixed set of commands can detect the version of those commands reliably, because it knows where to look. Letting an arbitrary binary through is a different problem, one where a version string has to be guessed from whatever the program prints, and the best effort wording in the documentation is a fair description of that. The whitelist is also the file that a new command has to be added to, which is why contributing support for more commands is listed as a way to help the project.

The trade-off is that has covers the tools its author cared about. A tool you use every day that is not on the list needs either the environment variable or a pull request.

A .hasrc file turns a directory into a tool manifest

The feature that makes has more than a one liner is the .hasrc file. When has runs, it looks for a file named .hasrc in the directory the command was issued from, and it treats the contents as a list of commands to check. One command per line, and lines beginning with a hash are comments.

bash
# tools
git
curl

# interpreters
ruby
node

Running has with no arguments inside a directory that holds such a file checks everything listed in it and prints the same version inventory.

bash
$ has

Command line arguments are additive rather than overriding, so passing extra tools on the command line checks those in addition to the ones in the file.

bash
$ has java

The suggestion in the documentation is to commit a .hasrc file at the root of a project, so that a quick run confirms the presence of every command line tool needed to build and run it. The repository keeps one for itself, which is a small piece of dogfooding that tells you the file is meant to be a real, maintained artifact rather than a demo.

The catch is that the file is read from the current directory only. Running the check from a subdirectory, or from outside the project, finds nothing, so the check is tied to where you stand rather than to where the project is.

Four install routes, from brew to a curl into /usr/local/bin

The installation section offers one method per audience. On macOS the shortest route is Homebrew, which is a single command.

bash
brew install has

For everyone else, the script can be cloned and installed with make, which needs no package manager at all. The root install writes to /usr/local by default and needs elevated permissions.

bash
git clone https://github.com/kdabir/has.git && cd has && sudo make install

The same clone works without root if you point PREFIX at a directory you own, which is the sensible choice for a shared machine or a container image.

bash
git clone https://github.com/kdabir/has.git
cd has
make PREFIX=$HOME/.local install

Or you can simply download the script to a file. The documentation notes that this command is safe to run more than once, which is also how you update an existing installation.

bash
curl -sL https://git.io/_has > /usr/local/bin/has

A tee based variant is given for the case where the shell is not writing to the target directly.

bash
curl -sL https://git.io/_has | sudo tee /usr/local/bin/has >/dev/null

Version manager users are covered too, with an asdf plugin and a mise command.

bash
asdf plugin add has https://github.com/sylvainmetayer/asdf-has
asdf install has 1.4.0
bash
mise use -g has@latest

Running has straight off the internet without installing it

The script can be piped from the network and executed with the tool names as arguments, which removes the install step entirely.

bash
curl -sL https://git.io/_has | bash -s git node npm

The output is the same version inventory as the installed form, which is what makes it usable as a one off check in a CI job or a container that has no package manager available.

The documentation pushes this further and suggests an alias so the short form works everywhere.

bash
alias has="curl -sL https://git.io/_has | bash -s"

With that in place, the command is typed the normal way and the shell expands the alias into the curl pipeline. The documentation shows the alias expansion when you ask the shell what has is.

bash
$ has git
$ type has

The same idea is offered for projects, where the .hasrc file of a checkout can be honoured on a machine that has no has installed at all, by running the piped command with no arguments from the project directory.

The security posture here is worth stating plainly. This pattern executes a script fetched over the network at the moment you run it, so the guarantee is whatever the remote endpoint serves at that instant rather than a version you pinned. That is a reasonable trade for a throwaway check and a poor one for a script that gates a build.

The Makefile is the real interface for maintainers

The script at the root of the repository is the only source file of the project, so the Makefile mostly exists to place that file and the shell completions on the system. The PREFIX variable defaults to /usr/local and can be overridden, and DESTDIR is honoured as well.

The user facing targets are install, install-completions, install-all, uninstall, uninstall-completions, uninstall-all, and update. Completions for bash and zsh are installed separately from the script, and the documentation gives the exact destinations they land in: a has file under the bash-completion completions directory and a _has file under the zsh site-functions directory.

bash
# Install everything (has + completions)
sudo make install-all

# Or install completions separately
sudo make install-completions
bash
make PREFIX=$HOME/.local install-all

The update target is the one that explains the structure of the project. Running make update fetches from upstream with git fetch and forces, and it removes the local has file first, because that file is the payload and the Makefile has a target that checks it out of git again.

bash
update : update-fetch has

Testing is done with bats, the bash test framework, and the Makefile splits the suite into unit tests and integration tests.

bash
test: unit-test intg-test
bash
unit-test:
	bats tests/unit/unit-tests.bats
	bats tests/unit/with-mocks.bats

The repository also carries a completions directory, a tests directory with both unit and integration suites, and a CONTRIBUTING.md that points at how to add support for more commands. The Makefile notes a practical wrinkle in a comment, that long options such as verbose do not work on macOS for cp, mkdir and rm, which is why the install targets use short flags.

A readable check is not a version gate

The limits of has follow from what it actually does, and they are worth stating before you build on it.

First, there is no version constraint. The script prints the version it detects, and the example output shows values such as a Node and a Java release, but there is no documented way to say that a project needs at least a given version. A script can read the printed line and compare it, but that is string handling in your script, not a feature of the tool. If your requirement is a version floor, has gives you the number and nothing else.

Second, there is no installation side to it. A missing tool is reported and counted, and the exit status tells you how many are missing, but nothing in the tool fetches or installs anything. That is a deliberate boundary for a script with no dependencies, and it is also why a missing tool cannot be a fatal error in the middle of a run.

Third, the whitelist is fixed. A tool that is not recognised is refused unless HAS_ALLOW_UNSAFE is exactly y, and the best effort version detection for an unknown command cannot be as reliable as for a known one.

Compared with a plain existence check in a shell script, has buys you three things: a readable per tool inventory with versions, a non zero exit code that counts the missing tools instead of collapsing them to one, and a project level .hasrc that records the requirement next to the code. What you give up is portability, since a plain command -v check needs nothing installed and no network.

The project is not archived, the last push was on 2026-09-11, and the release history shows v1.6.0 on 2026-09-11, v1.5.2 on 2025-08-01, and v1.5.1 on 2025-07-29. The licence is MIT, and the script is short enough that reading it end to end is a reasonable alternative to trusting it.

Editorial conclusion

Reach for has when you want a human readable answer to whether a machine has the tools a project needs, and when the exit status has to tell a script how many of them were absent. Do not reach for it as a version gate: it reports versions but the documentation shows no way to require a minimum version, and it has no way to install what is missing. Verify first that the tools you care about are on its whitelist, because an unrecognised command is refused unless you set HAS_ALLOW_UNSAFE=y, and check the .hasrc file in a cloned project before assuming its list of tools matches the ones the build actually calls.

Frequently asked questions

What does the has command do?

It checks whether the command line tools you name are present on your PATH and prints the version of each one it finds, one line per tool. The only dependency is bash itself.

What exit code does has return when a tool is missing?

The exit status reflects the number of commands that were not found, so a fully satisfied check returns 0 and a single missing tool returns 1. That makes the status usable as a count in a shell script.

How do I check a tool that has does not know?

Set the environment variable HAS_ALLOW_UNSAFE to exactly y, for example HAS_ALLOW_UNSAFE=y has foobar. The value must be exactly y, and the check then attempts the existence check and a best effort version detection.

What is the .hasrc file used for?

has looks for a .hasrc file in the directory you run it from and checks every command listed in it, one per line, with hash lines treated as comments. Command line arguments are added to that list rather than replacing it, and the documentation suggests committing a .hasrc at the root of a project to record its tool requirements.

How do I install has without Homebrew?

Clone the repository and run sudo make install, or use make PREFIX=$HOME/.local install for a non root install, or download the script directly with curl -sL https://git.io/_has > /usr/local/bin/has. Shell completions for bash and zsh are installed with make install-completions.

Official sources

  1. Issues
  2. kdabir/has on GitHub
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/kdabir-has.svg)](https://hysenlabs.com/projects/kdabir-has)

Was this useful?