Bazel rules and hermetic toolchain for Claude Code - Anthropic's AI coding assistant CLI. Run Claude prompts as build, test, and run actions, or use the toolchain to write your own rules.
Setup
Add the following to your MODULE.bazel:
bazel_dep(name = "rules_claude", version = "0.1.0") git_override( module_name = "rules_claude", remote = "https://github.com/buildbuddy-io/rules_claude.git", commit = "20a8309d6e3cce01a6ae5ae1b29cbd3390e484f9", )
The toolchain is automatically registered. By default, it downloads version 2.1.25 with SHA256 verification for reproducible builds.
Pinning a Claude Code version
To pin a specific Claude Code CLI version:
claude = use_extension("@rules_claude//claude:extensions.bzl", "claude") claude.download(version = "2.0.0")
Using the latest version
To always fetch the latest version:
claude = use_extension("@rules_claude//claude:extensions.bzl", "claude") claude.download(use_latest = True)
Running Claude Directly
To launch Claude Code interactively using the hermetic toolchain:
This runs the Claude CLI in interactive mode within your workspace.
Usage
load("@rules_claude//claude:defs.bzl", "claude", "claude_run", "claude_test") # Generate documentation from source files claude( name = "generate_docs", srcs = ["src/main.py"], prompt = "Generate markdown documentation for this Python module.", out = "docs.md", ) # Run a prompt with no input files claude( name = "hello", prompt = "Write a haiku about build systems.", ) # Summarize multiple files claude( name = "summary", srcs = [ "file1.txt", "file2.txt", ], prompt = "Summarize the key points from these files.", out = "summary.md", ) # Generate a complete static marketing website from a README claude( name = "website", srcs = ["README.md"], prompt = "Generate a complete static marketing website based on this README.", ) # Interactively refactor code with `bazel run` claude_run( name = "modernize", srcs = glob(["src/**/*.py"]), prompt = "Refactor this code to use modern Python 3.12 features like pattern matching and type hints.", ) # Deploy interactively claude_run( name = "deploy", srcs = ["main.go"], prompt = "Deploy this app to Google Cloud Run. Ask me for any credentials you need and give me links to where I can find them.", interactive = True, ) # Test that documentation is accurate claude_test( name = "validate_readme", srcs = ["README.md"], prompt = "Walk through this README and verify all the steps work correctly.", )
Authentication
Claude Code requires an ANTHROPIC_API_KEY to function. Since Bazel runs actions in a sandbox, you need to explicitly pass the API key through using --action_env.
Option 1: Pass from environment
To pass the API key from your shell environment, add to your .bazelrc:
common --action_env=ANTHROPIC_API_KEY
Then ensure ANTHROPIC_API_KEY is set in your shell before running Bazel.
Option 2: Hardcode in user.bazelrc
For convenience, you can hardcode the API key in a user.bazelrc file that is gitignored:
-
Add
user.bazelrcto your.gitignore:echo "user.bazelrc" >> .gitignore -
Create a
.bazelrcthat importsuser.bazelrc:echo "try-import %workspace%/user.bazelrc" >> .bazelrc -
Create
user.bazelrcwith your API key:common --action_env=ANTHROPIC_API_KEY=sk-ant-...
Option 3: Local Authentication
As an alternative to providing an API key, you can use local authentication to run Claude with your existing local credentials. This is useful when you already have Claude Code configured on your machine.
Enable local auth mode by adding this config to your .bazelrc:
common:local_auth --@rules_claude//:local_auth
common:local_auth --action_env=USER
Then use:
bazel build //my:target --config=local_auth
When local auth is enabled:
- The action runs locally (not sandboxed)
- Your real
HOMEandUSERenvironment variables are used, allowing Claude to access your local configuration and credentials
Rule Reference
claude
Runs Claude Code with the given prompt and input files to produce an output.
| Attribute | Type | Description |
|---|---|---|
srcs |
label_list |
Input files to be processed by the prompt. |
prompt |
string |
Required. The prompt to send to Claude. |
out |
string |
Output filename. If not specified, outputs to a directory. |
outs |
string_list |
Multiple output filenames. Takes precedence over out. |
local_auth |
label |
Flag to enable local auth mode. Defaults to @rules_claude//:local_auth. |
allowed_tools |
string_list |
List of allowed tools. If empty, uses --dangerously-skip-permissions. See permissions settings. |
claude_run
Creates an executable that runs Claude Code with the given prompt. Use with bazel run.
| Attribute | Type | Description |
|---|---|---|
srcs |
label_list |
Input files to be processed by the prompt. |
prompt |
string |
Required. The prompt to send to Claude. |
out |
string |
Output filename to include in the prompt. |
outs |
string_list |
Multiple output filenames to include in the prompt. |
allowed_tools |
string_list |
List of allowed tools. If empty, uses --dangerously-skip-permissions. See permissions settings. |
claude_test
Runs Claude Code with the given prompt as a Bazel test. The agent evaluates the prompt and writes a result file with PASS or FAIL on the first line, followed by an explanation.
| Attribute | Type | Description |
|---|---|---|
srcs |
label_list |
Input files to be processed by the prompt. |
prompt |
string |
Required. The prompt describing what to test and the pass/fail criteria. |
local_auth |
label |
Flag to enable local auth mode. Defaults to @rules_claude//:local_auth. |
allowed_tools |
string_list |
List of allowed tools. If empty, uses --dangerously-skip-permissions. See permissions settings. |
Toolchain API
The rules above are built on a hermetic, cross-platform toolchain that you can use directly to write your own rules.
In genrule
Use the toolchain in a genrule via toolchains and make variable expansion:
load("@rules_claude//claude:defs.bzl", "CLAUDE_TOOLCHAIN_TYPE") genrule( name = "my_genrule", srcs = ["input.py"], outs = ["output.md"], cmd = """ export HOME=.home $(CLAUDE_BINARY) --dangerously-skip-permissions -p \ 'Read $(location input.py) and write API documentation to $@' """, toolchains = [CLAUDE_TOOLCHAIN_TYPE], )
The $(CLAUDE_BINARY) make variable expands to the path of the Claude Code binary.
Note: The export HOME=.home line is required because Bazel runs genrules in a sandbox where the real home directory is not writable. Claude Code writes configuration and debug files to $HOME, so redirecting it to a writable location within the sandbox prevents permission errors. The --dangerously-skip-permissions flag allows Claude to read and write files without interactive approval.
In custom rules
Use the toolchain in your rule implementation:
load("@rules_claude//claude:defs.bzl", "CLAUDE_TOOLCHAIN_TYPE") def _my_rule_impl(ctx): toolchain = ctx.toolchains[CLAUDE_TOOLCHAIN_TYPE] claude_binary = toolchain.claude_info.binary out = ctx.actions.declare_file(ctx.label.name + ".md") ctx.actions.run( executable = claude_binary, arguments = [ "--dangerously-skip-permissions", "-p", "Read {} and write API documentation to {}".format(ctx.file.src.path, out.path), ], inputs = [ctx.file.src], outputs = [out], env = {"HOME": ".home"}, use_default_shell_env = True, ) return [DefaultInfo(files = depset([out]))] my_rule = rule( implementation = _my_rule_impl, attrs = { "src": attr.label(allow_single_file = True, mandatory = True), }, toolchains = [CLAUDE_TOOLCHAIN_TYPE], )
In tests
For tests that need to run the Claude binary at runtime, use the runtime toolchain type. This ensures the binary matches the target platform where the test executes:
load("@rules_claude//claude:defs.bzl", "CLAUDE_RUNTIME_TOOLCHAIN_TYPE") def _claude_test_impl(ctx): toolchain = ctx.toolchains[CLAUDE_RUNTIME_TOOLCHAIN_TYPE] claude_binary = toolchain.claude_info.binary test_script = ctx.actions.declare_file(ctx.label.name + ".sh") ctx.actions.write( output = test_script, content = """#!/bin/bash export HOME="$TEST_TMPDIR" {claude} --version """.format(claude = claude_binary.short_path), is_executable = True, ) return [DefaultInfo( executable = test_script, runfiles = ctx.runfiles(files = [claude_binary]), )] claude_test = rule( implementation = _claude_test_impl, test = True, toolchains = [CLAUDE_RUNTIME_TOOLCHAIN_TYPE], )
Toolchain types
There are two toolchain types depending on your use case:
-
CLAUDE_TOOLCHAIN_TYPE- Use for build-time actions (genrules, custom rules). Selected based on the execution platform. Use this when Claude's output isn't platform-specific. -
CLAUDE_RUNTIME_TOOLCHAIN_TYPE- Use for tests or run targets where the Claude binary executes on the target platform.
Public API
From @rules_claude//claude:defs.bzl:
| Symbol | Description |
|---|---|
claude |
Rule for running Claude prompts as build actions |
claude_run |
Rule for running Claude prompts with bazel run |
claude_test |
Rule for running Claude prompts as tests |
CLAUDE_TOOLCHAIN_TYPE |
Toolchain type for build actions (exec platform) |
CLAUDE_RUNTIME_TOOLCHAIN_TYPE |
Toolchain type for test/run (target platform) |
ClaudeInfo |
Provider with binary field containing the Claude Code executable |
claude_toolchain |
Rule for defining custom toolchain implementations |
LocalAuthInfo |
Provider for local auth flag |
local_auth_flag |
Rule for defining local auth build settings |
Supported platforms
darwin_arm64(macOS Apple Silicon)darwin_amd64(macOS Intel)linux_arm64linux_amd64
Requirements
- Bazel 7.0+ with bzlmod enabled
- Valid
ANTHROPIC_API_KEYenvironment variable, or local authentication enabled
Acknowledgements
Claude and Claude Code are trademarks of Anthropic, PBC.
