Session Integrations

June 16, 2026 · View on GitHub

Session integrations are optional ergonomics overlays for common technology stacks. They let Hazmat carry a small amount of stack-specific convenience into a session without weakening the containment model.

hazmat integration is the command surface for this feature.

What Integrations Can Do

  • Add read-only directories that are useful for a stack, such as toolchains or caches
  • Add snapshot exclude patterns for reproducible build artifacts
  • Pass through a small safe set of environment selectors and path pointers from the invoker environment
  • Show warnings or suggested commands for the stack

What Integrations Cannot Do

  • Widen project write scope
  • Bypass the seatbelt credential deny list
  • Change network policy
  • Inject arbitrary flags or preload-style environment variables
  • Reconfigure Claude/OpenCode runtime settings
block-beta
    columns 2
    block:allowed["Integrations CAN"]
        columns 1
        a1["Add read-only dirs"]
        a2["Add snapshot excludes"]
        a3["Pass safe env selectors"]
        a4["Show warnings"]
    end
    block:denied["Integrations CANNOT"]
        columns 1
        d1["Widen write scope"]
        d2["Expose denied credentials"]
        d3["Change network policy"]
        d4["Inject code or flags"]
    end

    style allowed fill:#dfd,stroke:#3a3,color:#000
    style denied fill:#fee,stroke:#c33,color:#000
    style a1 fill:#cfc,stroke:#3a3,color:#000
    style a2 fill:#cfc,stroke:#3a3,color:#000
    style a3 fill:#cfc,stroke:#3a3,color:#000
    style a4 fill:#cfc,stroke:#3a3,color:#000
    style d1 fill:#fcc,stroke:#c33,color:#000
    style d2 fill:#fcc,stroke:#c33,color:#000
    style d3 fill:#fcc,stroke:#c33,color:#000
    style d4 fill:#fcc,stroke:#c33,color:#000

This is the core design rule: integrations may reduce friction, but they may not weaken Hazmat's trust boundary.

Inspecting Integrations

hazmat integration list
hazmat integration show node
hazmat integration setup
hazmat integration scaffold bun --from-current-project
hazmat integration validate ~/.hazmat/integrations/bun.yaml

hazmat integration list shows built-in integrations, user-installed manifests under ~/.hazmat/integrations/, and any project pinning currently configured.

hazmat integration show <name> shows the integration's detect files, read-only paths, env passthrough keys, snapshot excludes, warnings, and command hints.

If the built-ins do not cover your stack, start with integration-contributor-flow.md. It shows how normal Hazmat output should lead users toward existing integrations, repo recommendations, or a small PR-shaped integration draft without relying on an exhaustive list of ecosystems.

hazmat integration setup prints the current project's integration state and the next commands for using, recommending, or creating integrations. hazmat integration scaffold <name> creates a valid draft manifest from the name you provide and optional current-project evidence. hazmat integration validate <file-or-name> checks a built-in, user-installed, or file-backed manifest before you use it or turn it into a PR.

Manifest session.read_dirs and session.env_passthrough are common to every platform. Platform-owned entries live under session.platforms.<platform>, so Darwin-specific Homebrew, Command Line Tools, java_home, /opt/homebrew, and /Library paths stay out of future Linux FHS or distro-package-manager rules. Only the current platform's overlay is merged into a session.

Activating Integrations

Activate an integration for a single session:

hazmat claude --integration node
hazmat opencode --integration go
hazmat shell --integration rust
hazmat exec --integration python-poetry -- poetry run pytest
hazmat exec --integration python-uv -- uv run pytest

If no integrations are active, Hazmat may suggest built-in integrations based on files in the project tree, such as go.mod, a nested frontend/package.json, or a *.cfg model file beside its .tla spec.

Explicit Project Access Extensions

Integrations are not the only way to shape a session. If you need additional directories, declare them directly:

hazmat claude -R ~/reference-docs
hazmat claude -W ~/.venvs/my-app
hazmat config access add -C ~/workspace/my-app --read ~/reference-docs --write ~/.venvs/my-app
hazmat config access remove -C ~/workspace/my-app --write ~/.venvs/my-app

Use this path-based access model when the directory is specific to your machine, writable, or too environment-specific to belong in a reusable integration.

Project Pinning

Pin integrations so they auto-activate for a specific project:

hazmat config set integrations.pin "~/workspace/my-app:node,go"
hazmat config set integrations.unpin ~/workspace/my-app

Hazmat canonicalizes the project path (Abs + EvalSymlinks) before storing the pin. At session start, the session's project path is resolved the same way and compared for exact equality. This means ~/workspace/my-app and /Users/dr/workspace/my-app both resolve to the same canonical pin. Re-running integrations.pin for the same project replaces the existing pin set.

Built-In Integrations

IntegrationDetectsRead dirsEnv passthroughSnapshot excludes
beads.beads/ (root dir)— (bd and dolt resolved via PATH; Homebrew permission repair on their Cellars when installed 0700).beads/dolt/, .beads/backup/, .beads/dolt-server.*, .beads/daemon.*, .beads/bd.sock*, .beads/*-lock, .beads/interactions.jsonl, .beads/ephemeral.sqlite3*, .beads/sync-state.json, .beads/push-state.json, .beads/export-state*, .beads/last-touched, .beads/.beads-credential-key, .beads/redirect, .beads/.local_version, .beads/.env
gogo.modresolved GOROOTGOPATH, GOPROXY, GOPRIVATE, CGO_ENABLEDvendor/
haskell-cabalcabal.project, *.cabal, stack.yamlresolved GHC and Cabal prefixesdist-newstyle/, .stack-work/
nodepackage.jsonresolved Node prefix; Darwin declares /opt/homebrew/lib/node_modulesNODE_ENVnode_modules/, .next/, .turbo/, .nuxt/, out/, .vercel/
pnpmpnpm-lock.yaml~/.local/share/pnpm, ~/Library/pnpm, ~/.pnpm-storePNPM_HOMEnode_modules/, .next/, .turbo/, .nuxt/, out/, .vercel/, .pnpm-store/
bunbun.lockb, bun.lock, bunfig.tomlresolved bun prefix (via LookPath("bun")); ~/.bunBUN_INSTALL, NODE_ENVnode_modules/, .next/, .nuxt/, dist/, out/
yarnyarn.lock~/.cache/yarn, ~/Library/Caches/Yarn, ~/.yarn, ~/.local/share/yarnYARN_CACHE_FOLDERnode_modules/, .next/, .turbo/, .nuxt/, out/, .vercel/, .pnp.cjs, .pnp.loader.mjs (zero-install .yarn/cache intentionally NOT excluded)
denodeno.json, deno.jsonc, deno.lockresolved deno prefix (via LookPath("deno")); ~/.cache/deno, ~/Library/Caches/deno, ~/.denoDENO_DIR, DENO_INSTALLcoverage/, .deno_dir/
kubernetes-renderChart.yaml, kustomization.yaml, helmfile.yaml~/Library/Caches/helm, ~/.cache/helm (render-only; KUBECONFIG/~/.kube intentionally NOT granted)— (env_passthrough empty by design)charts/*.tgz, .helm/, .kustomize/
python-piprequirements.txt (when neither uv.lock nor poetry.lock is at root)~/.cache/pip, ~/Library/Caches/pip, ~/.local/lib; resolved Python prefixVIRTUAL_ENV.venv/, venv/, env/, __pycache__/, .pytest_cache/, .mypy_cache/, .ruff_cache/, *.pyc, dist/, *.egg-info/, build/
python-poetrypoetry.lock~/.local/share/pypoetryVIRTUAL_ENV.venv/, __pycache__/, .pytest_cache/, .mypy_cache/, .ruff_cache/, *.pyc, dist/, *.egg-info/
python-uvuv.lock~/.local/share/uv, ~/.cache/uvVIRTUAL_ENV.venv/, __pycache__/, .pytest_cache/, .mypy_cache/, .ruff_cache/, .uv-cache/, *.pyc, dist/, *.egg-info/
huggingfaceHugging Face model metadata markers (config.json plus tokenizer_config.json, model_index.json, adapter_config.json plus adapter_model.safetensors, or sentence_bert_config.json)~/.cache/huggingface/hub, ~/.cache/huggingface/datasets, ~/.cache/huggingface/xetHF_HOME, HF_HUB_OFFLINE.cache/huggingface/
ollamaModelfile~/.ollama/models, ~/.ollama/cache, ~/.ollama/logs.ollama/models/, .ollama/cache/, .ollama/logs/
pytorch-torch-hubhubconf.py~/.cache/torch/hub, ~/.cache/torch/checkpoints.cache/torch/hub/, .cache/torch/checkpoints/
rustCargo.tomlresolved Rust sysrootRUSTUP_HOME, CARGO_HOME, CARGO_TARGET_DIRtarget/
java-gradlebuild.gradle, build.gradle.kts, settings.gradle, settings.gradle.ktsresolved JDK home and Gradle prefixJAVA_HOME.gradle/, build/, out/, target/, *.class
android-gradleAndroidManifest.xml, local.propertiesresolved Android SDK (via $ANDROID_HOME / $ANDROID_SDK_ROOT / ~/Library/Android/sdk / ~/Android/Sdk — the candidate must have a platforms/ subdir to count); ~/.android, ~/.gradleANDROID_HOME, ANDROID_SDK_ROOT, ANDROID_NDK_HOME, GRADLE_USER_HOMEbuild/, .gradle/, .cxx/, captures/, local.properties, *.iml, .idea/
cmakeCMakeLists.txtresolved cmake install prefix (via LookPath); resolved Xcode developer dir (via xcode-select -p for clang); ~/.cmakeCMAKE_PREFIX_PATH, CMAKE_GENERATORbuild/, cmake-build-debug/, cmake-build-release/, CMakeFiles/, CMakeCache.txt, _deps/, install/
dockerDockerfile, Containerfile, compose.yml, compose.yaml, docker-compose.yml, docker-compose.yaml, .devcontainer/ (root dir)— (Docker only works in Tier 3 Docker Sandbox mode; native containment never grants daemon access)DOCKER_HOST, DOCKER_TLS_VERIFY.docker/, *.dockerfile.swp
flutterpubspec.yamlresolved Flutter SDK root (via LookPath("flutter")); ~/.pub-cache, ~/.dart, ~/.dart-toolFLUTTER_ROOT, PUB_CACHEbuild/, .dart_tool/, .flutter-plugins, .flutter-plugins-dependencies, ios/Pods/, android/.gradle/, android/build/
java-mavenpom.xmlresolved JDK home and Maven prefixJAVA_HOMEtarget/, *.class
ruby-bundlerGemfile, Gemfile.lockresolved Ruby prefixvendor/bundle/, .bundle/, tmp/, log/
php-composercomposer.json~/.composer, ~/.config/composer, ~/.cache/composer, ~/Library/Caches/composerCOMPOSER_HOME, COMPOSER_CACHE_DIRvendor/, .phpunit.result.cache, var/cache/, var/log/
elixir-mixmix.exs, mix.lockresolved Elixir and Erlang prefixes_build/, deps/, erl_crash.dump
terraform-plan*.tf.terraform/, *.tfstate, *.tfstate.backup
opentofu-planmanual activationresolved OpenTofu prefix.terraform/, *.tfstate, *.tfstate.backup
tla-java*.cfg files with sibling *.tlaresolved JDK home; Darwin declares /Library/Java and /opt/homebrew/opt/openjdkJAVA_HOME, TLA2TOOLS_JARtla/states/, *.dot
swiftPackage.swiftresolved Xcode developer dir (via xcode-select -p); ~/Library/Caches/org.swift.swiftpm, ~/Library/org.swift.swiftpm, ~/Library/Developer/Xcode/DerivedDataDEVELOPER_DIR.build/, .swiftpm/, DerivedData/, Pods/, *.xcodeproj/xcuserdata/, *.xcworkspace/xcuserdata/
dotnetglobal.json, *.csproj, *.fsproj, *.vbproj, *.sln, Directory.Build.propsresolved .NET SDK root (via LookPath("dotnet")); ~/.nuget, ~/.dotnet, ~/.local/share/NuGetDOTNET_ROOT, NUGET_PACKAGESbin/, obj/, .vs/, TestResults/, packages/, *.user

Integrations influence three parts of session setup:

  1. Read-only access — toolchain and cache directories
  2. Pre-session snapshot excludes — reproducible build artifacts
  3. Safe environment passthrough — passive selectors from the invoker's environment

Hazmat prints integration-derived paths, snapshot excludes, registry redirect keys, and warnings at session start so the behavior stays visible.

For automation and compatibility checks, hazmat explain --json exposes the same integration state in machine-readable form. Use that instead of parsing the human-oriented contract text.

For some built-in integrations, Hazmat may also show a Host permission changes section in the session contract. This is used for narrowly-scoped, host-owned repairs such as a Homebrew toolchain permission fix that is required to make an already-approved read-only toolchain path executable by the agent. hazmat explain previews these repairs; it does not execute them.

Homebrew-backed resolution is therefore no longer purely observational. With host consent enabled, it may plan a persistent permission repair outside the project tree. Those repairs are visible in the contract before launch and are governed by tests and documentation rather than the current TLA+ suite.

Safe Environment Passthrough

Integrations may only request env keys from Hazmat's allowlist. The intent is to allow passive selectors and path pointers, not code-injection knobs.

Examples of allowed keys:

  • GOPATH
  • GOPROXY
  • RUSTUP_HOME
  • CARGO_HOME
  • VIRTUAL_ENV
  • JAVA_HOME

Examples of intentionally forbidden keys:

  • NODE_OPTIONS
  • PYTHONPATH
  • GOFLAGS
  • LD_PRELOAD
  • DYLD_INSERT_LIBRARIES
  • credential variables such as AWS_ACCESS_KEY_ID or GITHUB_TOKEN

Registry redirect keys like GOPROXY and NPM_CONFIG_REGISTRY are allowed but surfaced explicitly at session start because they change where downloads come from.

A repo can declare which integrations it needs in .hazmat/integrations.yaml:

integrations:
  - go
  - tla-java

This file is pure data: a list of existing integration names. No inline definitions, no custom paths, no env keys, no executable hooks.

Repo owns intent; host owns trust. Hazmat reads the file as a hint, not authority.

flowchart TD
    A[Session start] --> B{.hazmat/integrations.yaml exists?}
    B -- no --> C[Check --integration flags and config pins]
    B -- yes --> D[Compute SHA-256 of file]
    D --> E{Approved in ~/.hazmat/integration-approvals.yaml?}
    E -- "yes (path + hash match)" --> F[Activate recommended integrations]
    E -- no --> G[Prompt user: approve these integrations?]
    G -- yes --> H[Record approval] --> F
    G -- no --> I["Skip (print --integration hint)"]
    C --> J[Resolve and merge all active integrations]
    F --> J
    I --> J

On first encounter, it prompts:

hazmat: this repo recommends integrations: go, tla-java
hazmat: source: /Users/dr/workspace/hazmat/.hazmat/integrations.yaml
hazmat: approve these integrations for this repo? [y/N]

Approval is stored outside the repo in ~/.hazmat/integration-approvals.yaml, keyed by canonical project path + SHA-256 of the file contents:

  • Same repo + same file = no prompt (approved)
  • File changes (integration added or removed) = re-approve
  • Repo cloned to a different path = re-approve

If the user declines, integrations are not activated. They can still use --integration manually.

For Project Maintainers

To recommend integrations for your repo, add .hazmat/integrations.yaml:

integrations:
  - python-uv
  - go
  - node
  - tla-java

The file only lists names of existing built-in or user-installed manifests. It cannot define custom paths, env vars, or any session config inline.

Tell your contributors which integrations the repo needs, and note any prerequisites (runtimes, tools) in the project README. When a contributor runs hazmat claude for the first time, they'll see the approval prompt with the exact integration list.

If you want to add or improve an integration, start with integration-contributor-flow.md, then use integration-author-kit.md for the exact manifest contract.

If your project needs an integration that doesn't exist as a built-in, contributors can create a matching user manifest on their machines (see below). The .hazmat/integrations.yaml should still reference the integration name — it resolves through the same loader.

User Manifests

User-installed manifests live in:

~/.hazmat/integrations/<name>.yaml

Hazmat resolves names by checking built-ins first, then user manifests. This means you can extend or replace a built-in by creating a user manifest with the same name, or create entirely new integrations for stacks that Hazmat doesn't ship.

When to create a user manifest

  • A built-in integration is close but your environment differs (e.g., SDKMAN Java instead of Homebrew, or a custom Cargo registry)
  • Your project uses a stack that has no built-in integration
  • You need read-only access to a toolchain path specific to your machine

Writing a user manifest

An integration manifest is YAML with strict field validation. Unknown fields are rejected at load time.

integration:
  name: java-sdkman
  version: 1
  description: Java via SDKMAN (instead of Homebrew)

detect:
  files: [pom.xml, build.gradle]

session:
  read_dirs:
    - ~/.sdkman/candidates/java
  env_passthrough: [JAVA_HOME]
  platforms:
    darwin:
      read_dirs:
        - /Library/Java

backup:
  excludes:
    - .gradle/
    - build/
    - target/
    - "*.class"

warnings:
  - "Using SDKMAN Java. Ensure JAVA_HOME points to the correct version."

commands:
  build: ./gradlew build
  test: ./gradlew test

Fields reference:

FieldRequiredDescription
integration.nameyesLowercase alphanumeric + hyphens
integration.versionyesMust be 1
integration.descriptionnoOne-line description
detect.filesnoFilenames (no paths) that suggest this integration
detect.root_dirsnoProject-root-only directory markers (checked via os.Stat at projectDir/<name>, never recursed). Single path components only — no /, ., .., or whitespace. Use this for tools that only leave a top-level directory as a marker, such as .beads/
session.read_dirsnoPaths added read-only (~ expands to invoker home)
session.env_passthroughnoEnv var names from the safe allowlist only
session.platforms.darwin.read_dirsnoDarwin-only read dirs merged after common session.read_dirs
session.platforms.linux.read_dirsnoLinux-only read dirs for future Linux support
session.platforms.<platform>.env_passthroughnoPlatform-only env var names from the same safe allowlist
backup.excludesnoGlob patterns for snapshot exclusion
warningsnoMessages shown at session start
commandsnoName-to-command hints (informational, not executed)

Validation rules:

  • Read-only paths are canonicalized (Abs + EvalSymlinks) and checked against the credential deny list. Paths that resolve to ~/.ssh, ~/.aws, or other denied zones are rejected.
  • Env passthrough keys must be in the safe set (passive pointers like GOPATH, JAVA_HOME, VIRTUAL_ENV). Keys that accept arbitrary flags or preload code (NODE_OPTIONS, PYTHONPATH, GOFLAGS, LD_PRELOAD) are rejected. Credential/capability-shaped keys such as *_TOKEN, *_API_KEY, *_SECRET, *_PASSWORD, *_PRIVATE_KEY, *_ACCESS_KEY, and SSH_AUTH_SOCK are rejected before the safe-set check; they need a registry-backed SecretRef or brokered capability instead of env_passthrough.
  • Platform overlays are fail-closed. The schema currently accepts only darwin and linux, and Linux overlays remain inert until Linux sessions are implemented.
  • No negation in exclude patterns.
  • Manifest size limit: 8KB.

If any validation fails, hazmat rejects the entire manifest rather than partially applying it.

Combining multiple integrations

Activate multiple integrations in one session:

hazmat claude --integration node --integration python-uv

Or pin a combination:

hazmat config set integrations.pin "~/workspace/fullstack:node,python-uv"

Integrations merge additively. Read dirs, excludes, env passthrough, and warnings are unioned and deduplicated. If two integrations add the same read dir or exclude, it appears once.

Validation Matrix

The table below is the public-repo regression matrix for built-in session integrations. Use it when changing detection, runtime resolution, or Homebrew-backed read-only path derivation.

This is still a point-in-time snapshot as of April 4, 2026. Refresh it when repo layouts, popularity, or Homebrew formula availability shift. The checked-in source of truth for automation is testdata/stack-matrix/repos.yaml; keep this table and that manifest aligned.

RepoStackExpected integration(s)Key markersRelevant Homebrew formulasValidation notes
vercel/next.jsNode / TypeScript / Rustnode, rustpackage.json, nested Cargo.tomlnode, pnpm, local Rust toolchainHigh-signal mixed-stack target with .next output and real Rust crates in the build graph
pydantic/pydantic-aiPython / uvpython-uvuv.lockuvExercises uv run, virtualenv resolution, and Python toolchain traversal
python-poetry/poetryPython / Poetrypython-poetrypoetry.lockpoetryValidates Poetry-specific detection stays narrow
ollama/ollamaGogogo.modgoGood Go regression repo for go env GOROOT resolution
astral-sh/ruffRustrustCargo.tomllocal Rust toolchainFast Rust smoke-test target
jgm/pandocHaskell / Cabalhaskell-cabalcabal.project, *.cabalghc, cabal-installCanonical Haskell repo for Cabal detection and toolchain path derivation
PostgREST/postgrestHaskell servicehaskell-cabalcabal.project, *.cabalghc, cabal-install, libpqSecond Haskell shape with a service footprint rather than a CLI
spring-projects/spring-bootJava / Gradlejava-gradle, nodebuild.gradle(.kts), settings.gradle(.kts), package.jsonopenjdk, gradlePrimary Gradle validation target
apache/mavenJava / Mavenjava-mavenpom.xmlopenjdk, mavenCanonical Maven validation target
rails/railsRuby / Bundlerruby-bundler, nodeGemfile, Gemfile.lock, package.jsonrubyCanonical Bundler repo with a common mixed-stack layout
phoenixframework/phoenixElixir / Mixelixir-mix, nodemix.exs, mix.lock, package.jsonelixir, erlangCanonical Phoenix target for Mix detection
terraform-aws-modules/terraform-aws-vpcTerraform / HCLterraform-plan*.tfterraform or opentofuUseful Terraform-module shape for passive detection and formatter/validate flows
opentofu/opentofuOpenTofu / IaCopentofu-planexplicit --integration opentofu-planopentofuManual-activation target that reflects current Homebrew reality better than Terraform core
tlaplus/ExamplesTLA+ corpustla-java*.cfg with sibling *.tlaopenjdkCanonical corpus for the broadened TLA+ detector
Hazmat itselfGo + TLA+go, tla-javago.mod, *.cfg with sibling *.tlago, openjdkSelf-hosting target that should stay green

Suggested smoke tests:

  • Node repos: dependency install plus the repo's main build command
  • Python repos: pytest through the repo's package manager
  • Haskell repos: cabal build or a narrower package-level target
  • Java repos: wrapper-based build or test commands where present
  • Ruby repos: bundle exec test or console entrypoints
  • Elixir repos: mix test or mix compile
  • Terraform / OpenTofu repos: detection plus fmt or validate first; avoid apply in sandboxed runs
  • TLA+ repos: java -jar ... tla2tools.jar against a small model

The April 4, 2026 expansion items for Haskell, Java, Ruby, Elixir, OpenTofu, and broader TLA+ detection are now part of the built-in regression set. The next unsupported stacks worth evaluating are Bun-specific Node flows, Deno, SwiftPM, and OCaml/Dune.

Self-Hosting: Developing Hazmat Under Hazmat

Hazmat's own repo includes .hazmat/integrations.yaml recommending go and tla-java. On first hazmat claude in this repo, approve the recommended integrations and the session gets Go toolchain support plus Java paths for TLC model checking.

Prerequisites:

  • Go installed locally
  • Java 17+ installed locally (Homebrew: brew install openjdk)
  • ~/workspace/tla2tools.jar downloaded (see tla/VERIFIED.md)
  • ~/workspace as the sole entry in session.read_dirs

Further Reading

  • design-assumptions.md — credential storage zones, integration trust model, approval trust boundaries
  • threat-matrix.md — footnotes 9 and 10 cover repo recommendation approval threat analysis
  • usage.md — daily workflow including integrations
  • overview.md — tier selection and where integrations fit in the containment model