CLI tool
bazelbuild/bazel-watcher avatar
bazelbuild/bazel-watcher

ibazel rebuilds and restarts a Bazel target when your sources change

Tools for building Bazel targets when source files change.

518 stars145 forksGoApache-2.0

At a glance

What is it?
bazel-watcher provides ibazel, a source file watcher for Bazel projects that rebuilds a target on every save and can restart a running server, notify it on stdin, or drive fix commands from build output. It suits a Bazel developer who wants the save and test loop without running a full watch mode, and it now offers a versioned JSON notification alongside the legacy stdin protocol.
Who is it for?
ibazel fits a Bazel developer who wants a tight save-and-rebuild loop for one target, since it wraps the normal Bazel commands rather than replacing the build system, and the notification mode lets a server react to changes without being killed. It does not fit a project that needs Bazel's own watch mode or a monorepo-wide dependency graph, because ibazel watches a single target's graph.
Can I use it commercially?
Yes. Apache-2.0 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 17 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

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

Editorial analysis

What it does and how it starts

bazel-watcher is a source file watcher for Bazel projects, and the pitch is the developer loop.

Ever wanted to save a file and have your tests automatically run? How about restart your webserver when one of the source files change? Look no further.

The usage is one command.

bash
$ ibazel build //path/to/my:target

Save and your target will be rebuilt. The repository supports three commands, build, test and run, which is the same set of verbs Bazel itself offers for those operations, and that is the point of the design. ibazel does not replace Bazel's build graph or its incremental logic, it runs the ordinary commands repeatedly and reacts to what changed.

The practical consequence is that the loop is as incremental as Bazel is and no more. If a change invalidates a large part of the graph, ibazel waits for Bazel to rebuild it, because it is Bazel doing the work.

The README notes that this is not an official Google product, and the build status badge points at a Buildkite pipeline on the bazel organisation rather than a Google one. The project lives under the bazelbuild GitHub organisation and is Apache-2.0 licensed with the LICENSE file at the repository root.

The release history is active. The listed releases are v0.33.0 from 2026-08-26, v0.32.0 from 2026-07-23 and v0.31.0 from 2026-07-17, and the last push to the repository was on 2026-10-06.

Installing it four ways

There are several ways to install ibazel, and the choice matters more than usual because the binary has to sit on your PATH.

On macOS, from Homebrew.

sh
$ brew install ibazel

For a JavaScript developer, as a devDependency or by calling npm install directly in the project.

code
$ npm install @bazel/ibazel

On Linux, packages are available for Arch Linux through the aur/ibazel AUR package.

And you can compile it yourself with Bazel.

code
$ git clone [email protected]:bazelbuild/bazel-watcher
$ cd bazel-watcher
$ bazel build //cmd/ibazel

Which then has to be copied onto your path.

bash
$ export PATH=$PATH:$PWD/bazel-bin/cmd/ibazel/ibazel_

That export line ends with a trailing underscore character on the path component, which is the Bazel convention for a symlink to the real binary and is easy to miss when you copy it.

The npm route is worth noting for a specific reason. Installing it as a devDependency means the version is pinned in your lockfile, so every developer on the team gets the same watcher version, which removes a class of bug where one person's ibazel behaves differently from another's.

Notification mode, and the versioned event

By default, a target started with `ibazel run` will be terminated and restarted whenever it is notified of source changes.

There is an alternative, and it is the one you want for a server. If the build rule for your target contains `ibazel_notify_changes` in its tags attribute, then the command will stay alive and will receive a notification of the source changes on stdin.

Restarting a server on every save is crude. It works, but a server with warm caches and open connections loses all of that on each change, and the restart itself takes as long as the process startup. Notification mode lets the process decide what to do.

There is now a versioned form. Targets that additionally contain `ibazel_notify_changes_v1` receive a versioned JSON event after each incremental build containing the filesystem changes that triggered it. The instruction is to keep both tags so older iBazel versions still use notification mode, and targets without the new tag retain the legacy protocol.

The event format is one line of JSON.

text
IBAZEL_BUILD_COMPLETED SUCCESS
IBAZEL_EVENT {"version":1,"type":"build_completed","success":true,"changes":[{"path":"/workspace/src/app.ts","kind":"source"}]}

The kind field is `source` for source-file changes and `graph` for build-file changes, and that distinction is the useful one, since a build file change can invalidate the graph itself rather than just recompile a file.

The legacy protocol still exists alongside. IBAZEL_BUILD_COMPLETED SUCCESS on its own is the old notification, so a process that reads both can support old and new ibazel versions at once.

The catch-up build, and why startup order matters

There is one piece of startup behaviour worth knowing about, because it changes what your server sees at boot.

Notification-mode targets start before iBazel discovers their watch graph. iBazel installs the watchers afterward and sends a catch-up build, keeping a large watch query off the application's startup path without missing changes.

The reasoning is a performance one and it is a good one. Asking Bazel for the watch graph of a large target is not cheap, and doing it before launching the process would add that latency to every start. So the process starts first and the graph is computed afterwards, and the catch-up build covers the window between the two.

For an application, the practical effect is that it can receive a build-completed event very shortly after startup, before you would have expected it. If your server assumes it is running against a fully settled build, that assumption is wrong.

The correctness argument is the second half of the sentence, that this is done without missing changes. Without the catch-up build, changes made in the window between process start and watcher installation would be invisible, and the next build would silently include them without notifying anyone.

The output runner, and the file that drives it

iBazel is capable of producing and running commands from the output of Bazel commands. If iBazel is run with the flag `--run_output` then it will check for a `%WORKSPACE%/.bazel_fix_commands.json` and if present run any commands that match the provided regular expressions.

The example in the README is the Gazelle case, matching buildozer commands found in the output and providing a prompt to run the command, as well as invoking `bazel run //:gazelle` if it detects a missing import for your Go code.

code
[
  {
    "regex": "^Check that imports in Go sources match importpath attributes in deps.$",
    "command": "bazel",
    "args": [ "run", "//:gazelle" ]
  },
  {
    "regex": "^buildozer '(.*)'\\s+(.*)$",
    "command": "buildozer",
    "args": [ "$1", "$2" ]
  },
  {
    "regex": "^(\\S+)/[\\w-]+\\.proto:\\d+:\\d+: Import \"\\S+\" was not found or had errors\\.$",
    "command": "bazel",
    "args": [ "run", "//:gazelle", "--", "proto/$1" ]
  }
]

The three fields are documented. The regex is matched against every line of output, with a note that backslash characters need to be escaped once for the regex to be parsed properly. The command is run from the workspace root. The args list substitutes `$1` with the first match group of the regex, `$2` with the second, and `$0` for the entire match.

That substitution is what makes this useful rather than merely clever. The third entry captures a proto file path out of a compiler error and feeds it into the gazelle invocation, so the fix command is derived from the error rather than hardcoded.

There are two ways to turn it off, and the second is worth knowing about because it also silences the first-run instructions.

sh
echo "[]" > .bazel_fix_commands.json

An empty configuration file disables the output runner, and the README notes that this additionally disables the notification providing usage instructions on the first invocation of iBazel. If you would like to remove the banner but do not wish to configure the output runner, that single command does it.

Profiling, and what the events tell you

iBazel has a `--profile_dev` flag which turns on a generated profile output file about the build process and timing.

code
ibazel --profile_dev=profile.json run devserver

The profile outfile is in concatenated JSON format, with events outputted one per line, which means you can process it with a line-oriented tool rather than needing to parse a whole document.

The event table is a useful design in itself, because it shows what the watcher actually does in a cycle. There is IBAZEL_START, emitted when iBazel is started as part of the first iteration, carrying the iteration number, time, iBazel version, bazel version, max heap size and committed heap size.

Then a set of paired events. SOURCE_CHANGE when a source file change was detected, GRAPH_CHANGE when a build file change was detected, and each carries targets, elapsed time and the change itself. Then BUILD_START, BUILD_DONE, BUILD_FAILED for the build phase, TEST_START, TEST_DONE and TEST_FAILED for the test phase, and RUN_START, RUN_DONE and RUN_FAILED for the run phase.

And then the two that are specific to this tool. RELOAD_TRIGGERED is emitted when a livereload was triggered to any listening browsers, and REMOTE_EVENT is emitted when a remote event was received from the browser.

Those last two show the scope of the project. ibazel is not only a rebuild loop, it also has a browser side, with a websocket dependency and an example_client/ directory in the repository, so it can push reload notifications to the browsers watching your dev server.

Where ibazel is the wrong choice

Four cases argue against it.

The first is if you want Bazel's own watch mode. Bazel has a watch mode of its own and it is the more capable answer when it covers your case, because it understands the whole graph rather than one target. ibazel is a convenience layer that repeats Bazel commands, and the moment you want cross-target invalidation, remote caching aware restarts or Bazel's own execution semantics, you are better off with Bazel.

The second is if you need the output runner to run commands without review. The `--run_output` feature executes commands derived from build output, and by default it prompts for confirmation. The README notes that adding `--run_output_interactive=false` will run the command automatically without prompting. Turning that on means a build error message can cause a command to execute, which is a real risk when the regexes are loose.

The third is if you are on a platform without an install path. The documented options are Homebrew on macOS, npm, an AUR package on Arch, and building with Bazel. There is no mention of a package for Debian, Ubuntu or Fedora, so on those systems you are compiling from source.

The alternative in the same direction is a language-agnostic file watcher such as watchexec or entr, driving Bazel yourself, or an editor plugin that runs a Bazel command on save. The difference in approach is that ibazel knows about Bazel's targets, its build graph and its output, so it can restart the right process and parse Bazel's own error messages to drive fixes. A generic watcher gives you a file event and nothing else, which is fine for a script but means you reimplement the target tracking that ibazel does for you.

Editorial conclusion

ibazel fits a Bazel developer who wants a tight save-and-rebuild loop for one target, since it wraps the normal Bazel commands rather than replacing the build system, and the notification mode lets a server react to changes without being killed. It does not fit a project that needs Bazel's own watch mode or a monorepo-wide dependency graph, because ibazel watches a single target's graph. Install it with `brew install ibazel` or `npm install @bazel/ibazel`, tag your run rule with both `ibazel_notify_changes` and `ibazel_notify_changes_v1` to get the JSON event, and read the output runner section before you let it execute commands from your build log.

Frequently asked questions

What does ibazel do?

It is a source file watcher for Bazel projects that rebuilds a target when files change. The repository supports the build, test and run commands, so `ibazel build //path/to/my:target` rebuilds that target on every save, and `ibazel run` restarts the running target unless the rule asks for notification mode.

How do I install ibazel?

With `brew install ibazel` on macOS, `npm install @bazel/ibazel` as a project devDependency, the aur/ibazel AUR package on Arch Linux, or by building it yourself with `bazel build //cmd/ibazel` and adding the resulting binary to your PATH.

What is the difference between ibazel_notify_changes and ibazel_notify_changes_v1?

The first makes the target stay alive and receive a notification of source changes on stdin instead of being restarted. The second additionally receives a versioned JSON event after each incremental build containing the filesystem changes that triggered it, with kind set to source for source-file changes and graph for build-file changes. You should keep both tags so older iBazel versions still work.

What is the output runner in ibazel?

With the `--run_output` flag, ibazel looks for a `.bazel_fix_commands.json` in the workspace root and runs any command whose regex matches a line of the Bazel output, substituting $1, $2 and $0 from the match groups. Adding `--run_output_interactive=false` runs the commands without prompting, and an empty array in the file disables the feature and the first-run instructions.

What licence is bazel-watcher released under?

Apache-2.0, with the LICENSE file at the repository root. The README notes that this is not an official Google product, and the current release is v0.33.0 from 2026-08-26.

Official sources

  1. bazelbuild/bazel-watcher on GitHub
  2. Issues
  3. License: Apache-2.0
  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/bazelbuild-bazel-watcher.svg)](https://hysenlabs.com/projects/bazelbuild-bazel-watcher)

Was this useful?