Claude Code for Bazel Remote Cache (2026)

Claude Code for Bazel Remote Cache Workflow

Bazel’s incremental build capabilities are powerful, but even the fastest local builds can become bottlenecks in CI/CD pipelines and team environments. Remote caching transforms your build economy by sharing compilation artifacts across machines, and Claude Code can help you set up, manage, and optimize this workflow. This guide shows you how to integrate Claude Code into your Bazel remote cache setup for faster, more efficient builds.

Understanding Bazel Remote Caching

Bazel remote caching stores build outputs on a remote server instead of (or in addition to) your local machine. When another developer or CI runner needs a build artifact, Bazel downloads it from the cache instead of rebuilding from source. This dramatically reduces build times, especially for large monorepos with many interdependent targets.

There are two primary remote cache backends compatible with Bazel:

  • Remote Build Execution (RBE): Both caches and executes builds remotely
  • Remote Cache (RC): Only caches outputs, local execution

For most teams starting with remote caching, the cache-only approach is simpler to implement and provides immediate benefits without the complexity of remote execution.

Remote Cache vs. Disk Cache vs. RBE

Understanding which caching strategy fits your team is the first step before writing a single line of configuration.

Feature Disk Cache Remote Cache Remote Build Execution
Shared across machines No Yes Yes
Setup complexity Low Medium High
Requires network No Yes Yes
Execution offloading No No Yes
Best for Solo developers Small–large teams Large teams, CI scale
Typical cache hit rate 40–60% 70–90% 80–95%
Cost Free Server cost Higher infrastructure cost

Most teams see the biggest immediate win from remote caching without RBE. Once your hit rate plateaus and build queue times become the bottleneck, migrating to RBE is the natural next step.

How Bazel Computes Cache Keys

Bazel’s cache key for any action is a hash of:

  1. The action’s command line
  2. All declared input files (content-addressed, not timestamp-based)
  3. The environment variables declared in the action
  4. The execution platform and toolchain identifiers

This design means two machines with identical inputs always produce the same cache key. However, any undeclared dependency. a file read from disk without being declared in srcs or data, or an environment variable injected at build time. will cause cache misses or, worse, silently incorrect builds.

Claude Code is particularly useful here because it can audit your BUILD files for common patterns that leak undeclared inputs.

Setting Up Your Remote Cache

The most common remote cache implementations use either a gRPC-based protocol or HTTP/1.1. Here’s how to configure Bazel to use a remote cache with Claude Code assisting you:

Configuring the Bazelrc File

Create or modify your .bazelrc file to enable remote caching:

build --remote_cache=https://your-cache-server.example.com
build --remote_cache_header=Authorization Bearer YOUR_TOKEN
build --disk_cache=~/.bazel/cache

The disk_cache setting provides a local fallback, ensuring you have some caching even when the remote is unavailable.

A production-grade .bazelrc will separate concerns using config groups so engineers can opt in to remote caching without forcing it on every build:

.bazelrc
Always-on: local disk cache as a fallback
build --disk_cache=~/.bazel/cache
Opt-in remote cache (use: bazel build --config=remote //...)
build:remote --remote_cache=grpcs://your-cache-server.example.com:443
build:remote --remote_cache_header=Authorization=Bearer ${BAZEL_REMOTE_TOKEN}
build:remote --remote_timeout=30s
build:remote --remote_retries=2
CI profile: remote cache always enabled, uploads allowed
build:ci --config=remote
build:ci --remote_upload_local_results=true
Dev profile: remote cache for downloads only, no uploads
build:dev --config=remote
build:dev --remote_upload_local_results=false

Breaking the configuration this way achieves two goals. First, local developer builds stay fast when the remote cache is degraded. Second, only CI runners can write to the remote cache, which prevents half-built developer artifacts from poisoning the shared cache.

Setting Up a Simple Cache Server

For teams wanting to self-host, several options exist. The Bazel-Build-Event-Service (BES) can serve as a basic cache, or you can use specialized tools like Buildbarn or EngFlow. Claude Code can help you generate the appropriate Docker compose configuration:

version: '3'
services:
 redis:
 image: redis:7-alpine
 ports:
 - "6379:6379"
 volumes:
 - redis-data:/data
 bazel-cache:
 image: buildbarn/bb-browser
 ports:
 - "8080:8080"
 volumes:
 - cache-data:/data

For a more production-ready self-hosted solution, bazel-remote is the most commonly used standalone cache server. Here is a more complete Docker Compose setup that includes authentication and eviction settings:

version: '3.8'
services:
 bazel-remote:
 image: buchgr/bazel-remote-cache:latest
 ports:
 - "9090:9090" # gRPC
 - "8080:8080" # HTTP
 command:
 - --dir=/data
 - --max_size=50 # GB
 - --htpasswd_file=/etc/bazel-remote/.htpasswd
 - --tls_cert_file=/certs/server.crt
 - --tls_key_file=/certs/server.key
 volumes:
 - cache-data:/data
 - ./certs:/certs:ro
 - ./.htpasswd:/etc/bazel-remote/.htpasswd:ro
 restart: unless-stopped
volumes:
 cache-data:
 driver: local
 driver_opts:
 type: none
 device: /mnt/fast-ssd/bazel-cache
 o: bind

The --max_size flag enables LRU eviction so the cache disk usage stays bounded. Mounting the cache data directory on a fast SSD (or NVMe) significantly reduces the latency for cache reads.

Managed Remote Cache Options

If operating your own server is not practical, several managed options integrate with Bazel out of the box:

Provider Protocol Auth Free Tier
BuildBuddy Cloud gRPC + HTTP API key Yes (limited)
EngFlow gRPC OIDC No
Google Cloud Storage HTTP Service account Pay-per-use
AWS S3 (via bazel-remote) HTTP IAM Pay-per-use
Gradle Enterprise gRPC Token No

For teams already on GCP, using Google Cloud Storage as the cache backend requires zero additional infrastructure. Add the following to your .bazelrc and authenticate via Application Default Credentials:

build:remote --remote_cache=https://storage.googleapis.com/YOUR_BUCKET_NAME
build:remote --google_default_credentials

Creating a Claude Code Skill for Cache Management

A Claude Code skill can automate common cache operations, diagnose issues, and help optimize your cache hit rates. Here’s a skill structure for Bazel cache management:

Cache Status Skill

---
name: bazel-cache-status
description: Check and analyze Bazel remote cache status
---
Bazel Cache Status Checker
Check the current remote cache configuration and test connectivity.
Check Cache Configuration
Run this command to see your current cache settings:

The skill would then guide users through commands like:

bazel info | grep -i cache
bazel clean --expunge

A more complete version of this skill includes connectivity testing and configuration validation in a single sweep:

#!/usr/bin/env bash
cache-health.sh. run this via: claude --print "run cache-health.sh and summarize"
set -euo pipefail
CACHE_URL="${BAZEL_REMOTE_CACHE_URL:-}"
if [[ -z "$CACHE_URL" ]]; then
 echo "ERROR: BAZEL_REMOTE_CACHE_URL not set"
 exit 1
fi
echo "=== Bazel Cache Health Check ==="
echo ""
echo "--- Current bazelrc cache settings ---"
bazel info 2>/dev/null | grep -E "cache|remote" || echo "(no cache info)"
echo ""
echo "--- Connectivity test ---"
if curl -sf --max-time 5 "${CACHE_URL}/health" > /dev/null; then
 echo "PASS: cache server reachable"
else
 echo "FAIL: cannot reach ${CACHE_URL}"
fi
echo ""
echo "--- Disk cache size ---"
DISK_CACHE="${HOME}/.bazel/cache"
if [[ -d "$DISK_CACHE" ]]; then
 du -sh "$DISK_CACHE"
else
 echo "No local disk cache found at ${DISK_CACHE}"
fi
echo ""
echo "--- Recent build summary (last 5 builds) ---"
if [[ -f build_events.json ]]; then
 python3 - <<'PYEOF'
import json, sys
with open("build_events.json") as f:
 events = [json.loads(line) for line in f if line.strip()]
hits = sum(1 for e in events if e.get("id", {}).get("actionCompleted") and
 e.get("action", {}).get("type") == "MiddleMan" is False and
 e.get("action", {}).get("failureDetail") is None and
 "CacheHit" in str(e))
total = sum(1 for e in events if "actionCompleted" in str(e.get("id", {})))
print(f"Approximate cache hits: {hits}/{total}")
PYEOF
else
 echo "No build_events.json found. run with --build_event_json_file=build_events.json"
fi

Cache Hit Rate Analysis

Understanding your cache hit rate is crucial for optimization. Create a skill that parses build event logs to report cache performance:

def analyze_cache_performance(build_log_path):
 """Parse Bazel build events to calculate cache hit rate."""
 with open(build_log_path, 'r') as f:
 events = json.load(f)
 total_actions = 0
 cache_hits = 0
 for event in events:
 if 'action' in event:
 total_actions += 1
 if event['action'].get('cached'):
 cache_hits += 1
 hit_rate = (cache_hits / total_actions * 100) if total_actions > 0 else 0
 return f"Cache hit rate: {hit_rate:.1f}%"

Extending this to produce actionable output for Claude to analyze:

import json
import sys
from collections import defaultdict
from pathlib import Path
def analyze_cache_performance(build_log_path: str) -> dict:
 """
 Parse a Bazel Build Event Protocol JSON file and return
 per-mnemonic cache hit rates for Claude Code to interpret.
 """
 path = Path(build_log_path)
 if not path.exists():
 return {"error": f"File not found: {build_log_path}"}
 per_mnemonic: dict[str, dict[str, int]] = defaultdict(lambda: {"hits": 0, "misses": 0})
 total_actions = 0
 cache_hits = 0
 with open(path) as f:
 for line in f:
 line = line.strip()
 if not line:
 continue
 try:
 event = json.loads(line)
 except json.JSONDecodeError:
 continue
 action = event.get("action", {})
 if not action:
 continue
 total_actions += 1
 mnemonic = action.get("type", "Unknown")
 if action.get("cached") or action.get("cacheHit"):
 cache_hits += 1
 per_mnemonic[mnemonic]["hits"] += 1
 else:
 per_mnemonic[mnemonic]["misses"] += 1
 overall_rate = (cache_hits / total_actions * 100) if total_actions > 0 else 0
 # Sort by miss count descending so the worst offenders appear first
 sorted_mnemonics = sorted(
 per_mnemonic.items(),
 key=lambda kv: kv[1]["misses"],
 reverse=True
 )
 return {
 "overall_hit_rate": f"{overall_rate:.1f}%",
 "total_actions": total_actions,
 "cache_hits": cache_hits,
 "worst_offenders": [
 {
 "mnemonic": m,
 "hits": v["hits"],
 "misses": v["misses"],
 "hit_rate": f"{v['hits'] / (v['hits'] + v['misses']) * 100:.1f}%"
 }
 for m, v in sorted_mnemonics[:10]
 ]
 }
if __name__ == "__main__":
 result = analyze_cache_performance(sys.argv[1] if len(sys.argv) > 1 else "build_events.json")
 print(json.dumps(result, indent=2))

You can then pass this script’s output directly to Claude Code for interpretation and recommendations:

python3 analyze_cache.py build_events.json | \
 claude --print "Analyze these Bazel cache metrics and suggest the top 3 improvements"

Practical Workflows with Claude Code

Workflow 1: Initial Repository Setup

When setting up a new repository with Bazel and remote caching, Claude Code can guide you through the complete process:

  1. Initialize the Bazel workspace with bazel init
  2. Configure the .bazelversion file
  3. Set up the remote cache in .bazelrc
  4. Verify connectivity with a test build
  5. Document the setup in your team’s README

Here is a concrete example of what the initial workspace looks like after Claude Code has scaffolded it. The MODULE.bazel file (Bzlmod format, Bazel 6+) is typically the trickiest part for new users:

MODULE.bazel
module(
 name = "my_project",
 version = "0.1",
)
bazel_dep(name = "rules_go", version = "0.46.0")
bazel_dep(name = "gazelle", version = "0.35.0")
bazel_dep(name = "rules_python", version = "0.31.0")

And the companion .bazelversion that pins the exact Bazel release:

7.1.0

Pinning the Bazel version is important for cache correctness. If different developers run different Bazel versions, the toolchain identifier changes and cache keys diverge, causing every CI build to rebuild from scratch.

Workflow 2: Debugging Cache Misses

When builds aren’t caching as expected, Claude Code can help diagnose common issues:

  • Unmatched inputs: Check for timestamp-based or random inputs in build rules
  • Toolchain differences: Ensure consistent toolchains across machines
  • Action inputs: Review bazel aquery output to see what inputs Bazel considers
Query what inputs an action uses
bazel aquery '//some:target' --output=json | jq '.actions[].inputDepSets[]'

A systematic debugging session with Claude Code might look like this:

Step 1: Check whether the cache key changes between two runs
bazel aquery '//your/package:target' --output=text 2>&1 | sha256sum
Step 2: If the hash changes, find which input changed
bazel aquery '//your/package:target' --output=text > run1.txt
Make no changes, rebuild
bazel aquery '//your/package:target' --output=text > run2.txt
diff run1.txt run2.txt
Step 3: Look for volatile reads. genrules using $(date), $(git rev-parse HEAD), etc.
grep -r 'date\|git rev\|uname\|hostname' $(bazel info workspace)/BUILD* || true
Step 4: Check for environment variables leaking into actions
bazel aquery '//your/package:target' --output=json | \
 python3 -c "import json,sys; data=json.load(sys.stdin); \
 [print(a.get('environmentVariables','')) for a in data.get('actions',[])]"

Paste the diff output directly into a Claude Code session and ask it to explain which declared input changed and why. Claude is particularly good at identifying when a genrule embeds a timestamp or when a glob() pattern accidentally picks up .pyc or __pycache__ files that differ between machines.

Common root causes and fixes:

Root Cause Symptom Fix
stamp = True in cc_binary Always misses on fresh checkout Set stamp = 0 or use --nostamp
glob picks up generated files Misses after any build Add exclusion patterns to glob()
Env var in genrule cmd Misses on different machines Remove env var or declare it in env
Different Bazel versions Misses across CI and local Pin .bazelversion
Non-hermetic toolchain Misses on OS version changes Use rules_cc hermetic toolchain
ctx.actions.run_shell with date Always misses Replace with deterministic equivalent

Workflow 3: Optimizing Cache Usage

Claude Code can recommend optimizations based on your build patterns:

  • Modularize targets for better granularity
  • Use cc_shared_library for shared C++ dependencies
  • Configure fine-grained invalidation for generated files

Beyond those basics, there are several optimization patterns that Claude Code can help you implement systematically.

Split fat targets into smaller ones. A single cc_library that aggregates hundreds of source files means any change to any file invalidates the entire target. Breaking it up means only the changed sub-library needs rebuilding:

Before: one fat target
cc_library(
 name = "all_utils",
 srcs = glob(["/*.cc"]),
 hdrs = glob(["/*.h"]),
)
After: fine-grained targets
cc_library(
 name = "string_utils",
 srcs = ["string_utils.cc"],
 hdrs = ["string_utils.h"],
)
cc_library(
 name = "file_utils",
 srcs = ["file_utils.cc"],
 hdrs = ["file_utils.h"],
 deps = [":string_utils"],
)

Use exports_files to share headers without rebuilding. When many targets depend on the same header, having it as an explicit target prevents unnecessary rebuilds:

exports_files(["common_types.h"])
cc_library(
 name = "my_lib",
 hdrs = [":common_types.h"],
 srcs = ["my_lib.cc"],
)

Instrument your CI pipeline to report hit rates over time. A drop in hit rate is often the first signal that a refactor introduced a non-hermetic dependency:

.github/workflows/build.yml (excerpt)
- name: Build with remote cache
 run: |
 bazel build //... \
 --config=ci \
 --build_event_json_file=build_events.json
- name: Report cache hit rate
 run: python3 scripts/analyze_cache.py build_events.json >> $GITHUB_STEP_SUMMARY

Best Practices for Remote Cache Workflows

Authentication and Security

Always use authenticated connections to your remote cache, especially in production environments. Claude Code can help you set up credentials securely:

Store credentials in a secure location
export BAZEL_REMOTE_CACHE_KEY="$(cat ~/.bazel/cache-key)"

For CI environments, use short-lived tokens rather than static API keys where possible. Most managed cache providers support OIDC token exchange, which eliminates the need to store secrets at all:

GitHub Actions example: OIDC-based auth for BuildBuddy
- name: Authenticate to remote cache
 uses: google-github-actions/auth@v2
 with:
 workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
 service_account: ${{ secrets.SA_EMAIL }}
- name: Build
 run: |
 bazel build //... \
 --config=ci \
 --google_default_credentials

For self-hosted bazel-remote, restrict write access by IP range or require a client certificate. Cache poisoning. where a malicious or broken build writes incorrect artifacts. is a real risk in shared environments. A useful defensive .bazelrc pattern for local developers:

Developer machines: read-only access to the shared cache
build:dev --remote_upload_local_results=false
CI only: write access via a separate token stored in CI secrets
build:ci --remote_upload_local_results=true
build:ci --remote_cache_header=Authorization=Bearer ${CI_CACHE_WRITE_TOKEN}

Cache Invalidation Strategy

Sometimes you need to intentionally invalidate cache entries. Create a skill that handles this:

---
name: bazel-cache-invalidate
description: Safely invalidate Bazel cache entries
---
Cache Invalidation Helper
When you need to invalidate specific targets, use:
Invalidate specific targets
bazel clean --experimental_force_clean //target:to_invalidate
For complete cache reset (use carefully)
bazel clean --expunge

A more targeted approach avoids nuking the entire cache. Bazel does not provide a built-in “delete this key” command for remote caches, but you can force a re-upload by slightly modifying the action’s declared inputs or environment. The cleanest production pattern is to use a cache namespace (sometimes called a “cache silo”):

Rotate the cache namespace to force a clean slate for all users
build --remote_default_exec_properties=cache-silo=2026-03-15

Changing the cache-silo value effectively invalidates every cached artifact without deleting anything from the storage backend. Old entries will expire via LRU eviction.

Monitoring and Alerts

Integrate cache monitoring into your CI/CD pipeline:

build:ci:
 # Run with remote cache
 build --remote_cache=$CACHE_URL
 # Report cache statistics
 build --build_event_json_file=build_events.json

Then parse the JSON to extract cache hit rates and alert on degradation.

A complete GitHub Actions monitoring job that posts a summary and fails if the hit rate drops below a threshold:

- name: Check cache health
 run: |
 python3 - <<'EOF'
 import json, sys
 from pathlib import Path
 events_path = Path("build_events.json")
 if not events_path.exists():
 print("No build_events.json found, skipping cache health check")
 sys.exit(0)
 hits = misses = 0
 with open(events_path) as f:
 for line in f:
 try:
 e = json.loads(line)
 except Exception:
 continue
 action = e.get("action", {})
 if not action:
 continue
 if action.get("cached") or action.get("cacheHit"):
 hits += 1
 else:
 misses += 1
 total = hits + misses
 rate = (hits / total * 100) if total else 0
 print(f"Cache hit rate: {rate:.1f}% ({hits}/{total})")
 THRESHOLD = 60
 if total > 50 and rate < THRESHOLD:
 print(f"ALERT: Cache hit rate {rate:.1f}% is below threshold {THRESHOLD}%")
 sys.exit(1)
 EOF

Setting up a Datadog or Grafana dashboard for cache hit rates over time gives you the long-term visibility you need to catch regressions before they affect developer productivity.

Troubleshooting Common Issues

Build Fails When Remote Cache Is Unavailable

By default Bazel treats a remote cache failure as a build failure. Add --remote_local_fallback to fall back to local execution gracefully:

build:remote --remote_local_fallback
build:remote --remote_local_fallback_strategy=local

TLS Certificate Errors

Self-signed certificates are a common problem. You can provide a custom CA cert:

build:remote --tls_client_certificate=/path/to/client.crt
build:remote --tls_client_key=/path/to/client.key
build:remote --remote_cache=grpcs://cache.internal:443

Or, for development only (never production), disable TLS verification:

build:dev-insecure --remote_cache=grpc://cache.internal:9090

Cache Writes Timing Out in CI

Large artifacts can time out on slow upload links. Increase the timeout and limit parallelism:

build:ci --remote_timeout=120s
build:ci --jobs=8

The --jobs flag controls how many concurrent Bazel actions run, which indirectly caps the number of simultaneous cache uploads.

Conclusion

Integrating Claude Code with Bazel remote caching creates a powerful workflow for build optimization. By automating cache management tasks, debugging issues, and providing actionable insights, Claude Code helps your team achieve faster builds and better developer experience. Start with a simple cache configuration, use skills to manage common operations, and progressively optimize as your build patterns mature.

The key is starting simple, configure a basic remote cache, verify it works, then layer on Claude Code skills to handle the operational complexities. Use the cache hit rate analysis scripts to identify your worst-performing targets, consult Claude Code to understand why those targets miss, and apply the targeted fixes from the troubleshooting table. Your team will thank you when those build times drop from minutes to seconds, and your CI queue times shrink to match.


Try it: Paste your error into our Error Diagnostic for an instant fix.

I'm a solo developer in Vietnam. 50K Chrome extension users. $500K+ on Upwork. 5 Claude Max subscriptions running agent fleets in parallel. These are my actual CLAUDE.md templates, orchestration configs, and prompts. Not a course. Not theory. The files I copy into every project before I write a line of code. **[See what's inside →](https://zovo.one/lifetime?utm_source=ccg&utm_medium=cta-default&utm_campaign=claude-code-for-bazel-remote-cache-workflow)** $99 once. Free forever. 47/500 founding spots left.

Related Reading

Built by theluckystrike. More at zovo.one

Get started → Generate your project setup with our Project Starter.