{ config, lib, osConfig, pkgs, ... }: let claude-code = pkgs.unstable.claude-code; # Endpoint and wire protocol come straight off the hive's own OTLP config in # nixosModules/constellation-swarm.nix, so there is one place to change them. # Only those two are shared: the hive's `agent=` / `hive=` / `swarm=` resource # labels are deliberately not carried over. hiveOtel = osConfig.services.hyperhive.otel; # Written by the `claude-otel-user-header` oneshot in that same module, which # copies the hive's root-only auth header into /run owned by this user. # Absent on hosts without `my.constellation-swarm`, where the wrapper starts # claude with no telemetry at all rather than retrying an unauthenticated # export every interval. headerFile = "${osConfig.my.constellation-swarm.userOtelHeaderDir}/${config.home.username}"; # OTEL's `host.arch` is an enum of its own spelling, not uname's. hostArch = { x86_64-linux = "amd64"; aarch64-linux = "arm64"; } .${pkgs.stdenv.hostPlatform.system} or pkgs.stdenv.hostPlatform.uname.processor; # `deployment.environment.name` is what keeps these interactive sessions # apart from the agent fleet in the same backend, now that the hive's own # identifying labels are gone. resourceAttributes = lib.concatStringsSep "," [ "service.name=claude-code" "host.arch=${hostArch}" "os.type=linux" "deployment.environment.name=workstation" ]; # Env claude-code reads to export telemetry. Set by the wrapper rather than # written into ~/.claude/settings.json, because that file is self-mutating — # /model, /config and plugin toggles all write to it, so home-manager cannot # own it without breaking them. otelEnv = { CLAUDE_CODE_ENABLE_TELEMETRY = "1"; OTEL_METRICS_EXPORTER = "otlp"; # Interactive sessions carry far more sensitive content than hive agents, # so prompt events and tool-decision records never leave the machine. OTEL_LOGS_EXPORTER = "none"; OTEL_TRACES_EXPORTER = "none"; OTEL_EXPORTER_OTLP_PROTOCOL = hiveOtel.protocol; OTEL_EXPORTER_OTLP_ENDPOINT = hiveOtel.endpoint; # claude-code defaults to DELTA, which Prometheus/Mimir-family backends # silently drop without a deltatocumulative processor. OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE = "cumulative"; OTEL_METRICS_INCLUDE_VERSION = "1"; } # Follow the hive's export cadence too, so retuning it stays a one-line # change over in constellation-swarm.nix. Null there leaves claude-code's # own 60s default. `debug` is pointedly not mirrored: the OTEL SDK's stderr # diagnostics would scribble over an interactive TUI. // lib.optionalAttrs (hiveOtel.metricIntervalMs != null) { OTEL_METRIC_EXPORT_INTERVAL = toString hiveOtel.metricIntervalMs; }; exports = lib.concatStrings ( lib.mapAttrsToList (name: value: " export ${name}=${lib.escapeShellArg value}\n") otelEnv ); # Shadows claude-code's own `bin/claude`, so claude-code itself is kept out # of home.packages (see ./default.nix) — two derivations installing the same # binary is a home-manager collision, not a precedence win. claude-wrapper = pkgs.writeShellScriptBin "claude" '' hdr=${lib.escapeShellArg headerFile} if [ -r "$hdr" ]; then ${exports} export OTEL_EXPORTER_OTLP_HEADERS="$(cat "$hdr")" export OTEL_RESOURCE_ATTRIBUTES=${lib.escapeShellArg resourceAttributes}",host.name=$(${pkgs.coreutils}/bin/uname -n)" fi exec ${claude-code}/bin/claude "$@" ''; in { home.packages = [ claude-wrapper ]; }