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
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.
$ has node npm java git gradleEach 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.
$ has node go javacThe 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.
$ echo $?
1That 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.
# tools
git
curl
# interpreters
ruby
nodeRunning has with no arguments inside a directory that holds such a file checks everything listed in it and prints the same version inventory.
$ hasCommand 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.
$ has javaThe 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.
brew install hasFor 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.
git clone https://github.com/kdabir/has.git && cd has && sudo make installThe 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.
git clone https://github.com/kdabir/has.git
cd has
make PREFIX=$HOME/.local installOr 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.
curl -sL https://git.io/_has > /usr/local/bin/hasA tee based variant is given for the case where the shell is not writing to the target directly.
curl -sL https://git.io/_has | sudo tee /usr/local/bin/has >/dev/nullVersion manager users are covered too, with an asdf plugin and a mise command.
asdf plugin add has https://github.com/sylvainmetayer/asdf-has
asdf install has 1.4.0mise use -g has@latestRunning 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.
curl -sL https://git.io/_has | bash -s git node npmThe 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.
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.
$ has git
$ type hasThe 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.
# Install everything (has + completions)
sudo make install-all
# Or install completions separately
sudo make install-completionsmake PREFIX=$HOME/.local install-allThe 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.
update : update-fetch hasTesting is done with bats, the bash test framework, and the Makefile splits the suite into unit tests and integration tests.
test: unit-test intg-testunit-test:
bats tests/unit/unit-tests.bats
bats tests/unit/with-mocks.batsThe 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
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.
[](https://hysenlabs.com/projects/kdabir-has)Was this useful?