> ## Documentation Index
> Fetch the complete documentation index at: https://site.aspect.build/llms.txt
> Use this file to discover all available pages before exploring further.

# Device management: tools on your developers PATH

> Explore easier developer tool distribution with Bazel and direnv for seamless environment setup and management in your development workflow

export const BlogPost = ({title, date, authors, tags, image, children}) => {
  const tagList = tags ? tags.split(", ").filter(Boolean) : [];
  const tagSlug = t => t.toLowerCase().replace(/&/g, "").replace(/\+/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
  const formattedDate = date ? new Date(date + "T00:00:00").toLocaleDateString("en-US", {
    year: "numeric",
    month: "long",
    day: "numeric"
  }) : "";
  return <section className="w-full flex justify-center px-4 py-12 md:py-16">
      <div style={{
    maxWidth: "800px",
    width: "100%"
  }}>
        {image && (typeof image === "string" ? <img noZoom src={image} alt={title} className="w-full rounded-xl mb-8" style={{
    maxHeight: "400px",
    objectFit: "cover"
  }} /> : <div className="blog-post-hero-image">{image}</div>)}
        <h1 className="text-3xl md:text-4xl font-bold text-zinc-900 dark:text-white">
          {title}
        </h1>
        <div className="flex flex-wrap items-center gap-3 mt-4 text-sm text-zinc-500 dark:text-zinc-400">
          {authors && <span>{authors}</span>}
          {authors && formattedDate && <span>·</span>}
          {formattedDate && <span>{formattedDate}</span>}
        </div>
        {tagList.length > 0 && <div className="flex flex-wrap gap-2 mt-3">
            {tagList.map(tag => <a key={tag} href={"/blog/tags/" + tagSlug(tag)} className="px-2 py-0.5 rounded-full text-xs bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 hover:bg-blue-100 dark:hover:bg-blue-900/40 hover:text-blue-700 dark:hover:text-blue-300 transition">
                {tag}
              </a>)}
          </div>}
        <hr className="my-8 border-zinc-200 dark:border-zinc-700" />
        <div className="prose dark:prose-invert max-w-none">{children}</div>
      </div>
    </section>;
};

export const MarketingPage = () => <div className="marketing-page-marker" style={{
  display: "none"
}} />;

export const Section = ({children, className = "", gray = false, dark = false, id}) => <section id={id} className={`w-full flex justify-center px-4 py-16 md:py-24 ${gray ? "bg-gray-50 dark:bg-zinc-900" : dark ? "bg-zinc-900 dark:bg-zinc-950" : ""} ${className}`}>
    <div className="w-full" style={{
  maxWidth: "1140px"
}}>
      {children}
    </div>
  </section>;

<MarketingPage />

<BlogPost title="Device management: tools on your developers PATH" date="2025-05-13" authors="Alex Eagle" tags="Bazel, Aspect CLI">
  <img noZoom src="https://mintcdn.com/aspectbuild/x1L7Iep716jCyJVo/images/blog/hashnode/hn-07d3b9a0c1.jpeg?fit=max&auto=format&n=x1L7Iep716jCyJVo&q=85&s=29e8c046447aaded998231ce2a1acd27" alt="" className="blog-post-cover" width="1296" height="810" data-path="images/blog/hashnode/hn-07d3b9a0c1.jpeg" />

  “Device Management”, or MDM, is that thing which forces your work computer to have security software installed. It has the ability to push tools to developer machines too - however it’s owned by the security team at your company. While it would be convenient, I’ve found it’s difficult for the Developer Platform team to use that to distribute the "canonical developer environment”.

  Some folks use a devcontainer or VDI (Virtual Desktop Infrastructure) to describe the tooling you need installed, but that’s heavy and slow. If we’re using Bazel, it already has features to give a hermetic environment, right?

  A year ago, I wrote an article describing how to get tools on your developers local machine using Bazel, and today I have an update.

  In the technique from my original post ([run tools installed by bazel](/blog/run-tools-installed-by-bazel)), users have to change their behavior, typing `./tools/my-tool` rather than just `my-tool`. Retraining developers to have a different command under their fingers is hard, especially for things they run all the time.

  Shortly after I wrote that post, Fabian Meumertzheim created [https://github.com/buildbuddy-io/bazel\_env.bzl](https://github.com/buildbuddy-io/bazel_env.bzl). This is an alternative technique I’ll write about today. It puts the tools on the `$PATH` instead, fixing the ergonomic issue from the first technique — however there are always trade-offs! This one requires engineers manually install the `direnv` tool before they get setup.

  ## How direnv works

  **direnv** is a command-line tool that automatically sets and unsets environment variables when you `cd` into or out of a directory. It’s especially useful for managing project-specific environment variables, such as secrets, configuration settings, or language versions (like Python or Node.js versions).

  * You place a `.envrc` file in your project directory.

  * This `.envrc` file contains shell commands to export environment variables or run setup scripts.

  * When you enter the directory, `direnv` loads the `.envrc` file and applies the environment changes.

  * When you leave the directory, it automatically reverts those changes.

  Here’s how it looks when I enter Aspect’s monorepo (named “silo”):

  ```plaintext theme={null}
  alexeagle@aspect-build ~ % cd Projects/silo
  direnv: loading ~/Projects/silo/.envrc
  direnv: export ~PATH
  ```

  Installing `direnv` isn’t just a matter of getting the program on your machine. It also needs to hook into your shell (it supports `bash`, `zsh` and many others). And finally, you must explicitly allow each `.envrc` to be trusted.

  To follow this pattern, you’ll need to instruct your developers to install this first tool manually. Fortunately they’ll get a reminder of the instructions in the next step.

  ## bazel\_env creates a .envrc

  After installing `bazel_env.bzl` you’ll have a runnable Bazel target, typically `bazel run //:_bazel_env` or `bazel run //tools:bazel_env`.

  The `bazel_env` target is defined with a dictionary that maps a tool name to put on the PATH, to some other target that provides it. What kinds of targets can those be? Take a look at the example: [https://github.com/buildbuddy-io/bazel\_env.bzl/blob/main/examples/BUILD.bazel](https://github.com/buildbuddy-io/bazel_env.bzl/blob/main/examples/BUILD.bazel)

  1. Binary targets for programs you author yourself in the monorepo

  2. Tools provided by a toolchain, like `go`, `node`, `pnpm`, `cargo`, etc

  3. With [https://github.com/theoremlp/rules\_multitool](https://github.com/theoremlp/rules_multitool) you can run `multitool` to update a `tools.lock.json` file, and all of these tools are installed.

  4. CLI utilities distributed by a package manager, like console scripts from PyPI, `bin` entries from the `package.json` of NPM packages, Go utilities (see `scaffold` example here), and so on.

  ```plaintext theme={null}
  % bazel run //tools:bazel_env
  INFO: Analyzed target //tools:bazel_env (580 packages loaded, 72963 targets configured).
  INFO: Found 1 target...
  Target //tools:bazel_env up-to-date:
    bazel-bin/tools/bazel_env_all_tools
  INFO: Elapsed time: 9.399s, Critical Path: 0.45s
  INFO: Running command line: bazel-bin/tools/bazel_env.sh

  ====== bazel_env ======

  ✅ direnv is installed
  ✅ direnv added bazel-out/bazel_env-opt/bin/tools/bazel_env/bin to PATH

  Tools available in PATH:
    * aws:             @aws
    * pnpm:            @pnpm
    * gofumpt:         @@rules_multitool~~multitool~multitool//tools/gofumpt:gofumpt
    * jsonnetfmt:      @@rules_multitool~~multitool~multitool//tools/jsonnetfmt:jsonnetfmt
    * shfmt:           @@rules_multitool~~multitool~multitool//tools/shfmt:shfmt
    * terraform:       //tools:terraform
    * yamlfmt:         @@rules_multitool~~multitool~multitool//tools/yamlfmt:yamlfmt
    * ruff:            @@rules_multitool~~multitool~multitool//tools/ruff:ruff
    * shellcheck:      @@rules_multitool~~multitool~multitool//tools/shellcheck:shellcheck
    * buf:             @@rules_multitool~~multitool~multitool//tools/buf:buf
    * buildozer:       @@rules_multitool~~multitool~multitool//tools/buildozer:buildozer
    * docker-compose:  @@rules_multitool~~multitool~multitool//tools/docker-compose:docker-compose
    * diesel-cli:      @@rules_multitool~~multitool~multitool//tools/diesel-cli:diesel-cli
    * etcdctl:         @@rules_multitool~~multitool~multitool//tools/etcdctl:etcdctl
    * grpcurl:         @@rules_multitool~~multitool~multitool//tools/grpcurl:grpcurl
    * ibazel:          @@rules_multitool~~multitool~multitool//tools/ibazel:ibazel
    * multitool:       @@rules_multitool~~multitool~multitool//tools/multitool:multitool
    * otel-cli:        @@rules_multitool~~multitool~multitool//tools/otel-cli:otel-cli
    * otelcol-contrib: @@rules_multitool~~multitool~multitool//tools/otelcol-contrib:otelcol-contrib
    * promtool:        @@rules_multitool~~multitool~multitool//tools/promtool:promtool
    * pyrra:           @@rules_multitool~~multitool~multitool//tools/pyrra:pyrra
    * tflint:          @@rules_multitool~~multitool~multitool//tools/tflint:tflint
    * tfsec:           @@rules_multitool~~multitool~multitool//tools/tfsec:tfsec
    * buildifier:      @buildifier_prebuilt//:buildifier
    * scaffold:        @com_github_hay_kot_scaffold//:scaffold
    * node:            $(NODE_PATH)
    * cargo:           $(CARGO)
    * rustfmt:         $(RUSTFMT)

  Toolchains available at stable relative paths:
    * nodejs: bazel-out/bazel_env-opt/bin/tools/bazel_env/toolchains/nodejs
    * rust:   bazel-out/bazel_env-opt/bin/tools/bazel_env/toolchains/rust

  direnv: loading ~/Projects/silo/.envrc
  direnv: export ~PATH
  ```

  ## Guardrails

  There are a few things that can go wrong, which are worth pointing out.

  You’ll note that the tools are actually installed under a named output folder, `bazel-out/bazel_env-opt` - what if the user runs a `bazel clean`? This case is well handled, since `direnv` is able to report errors in the `.envrc` file with a custom message:

  ```plaintext theme={null}
  alexeagle@aspect-build silo % bazel clean
  INFO: Starting clean (this may take a while). Consider using --async if the clean takes more than several minutes.
  direnv: loading ~/Projects/silo/.envrc
  direnv: ERROR[bazel_env.bzl]: Run 'bazel run //tools:bazel_env' to regenerate bazel-out/bazel_env-opt/bin/tools/bazel_env/bin
  direnv: export ~PATH
  ```

  Any time the tools change, everyone has to run it again.

  It’s also not lazy (at least not yet). When you run `bazel_env` it needs to fetch all the tools, even those you never plan to run. See [https://github.com/buildbuddy-io/bazel\_env.bzl/issues/14](https://github.com/buildbuddy-io/bazel_env.bzl/issues/14)

  As a workaround, you can have multiple `bazel_env` targets, but users have to choose the right one for the work they intend to do.
</BlogPost>
