feat(coding-agent): auto-load direnv environment into the bash session

The bash tool's persistent shell didn't carry a repo's direnv/devenv
environment, so devenv-provided tools (moon, project-pinned biome/bun,
toolchains) were off PATH and .envrc-set vars (e.g. GIT_DIR for a jj
secondary workspace) were missing. Resolve the nearest .envrc from the
run cwd, load its env via direnv export json, and merge it under the
caller's per-call env. Gated by bash.direnv (default auto, auto-allows).
This commit is contained in:
Matt Wilkinson
2026-07-08 23:48:05 -04:00
parent f25ab54c59
commit 47493b0742
4 changed files with 269 additions and 1 deletions
@@ -10,6 +10,7 @@ import { Settings, type ShellMinimizerSettings } from "../config/settings";
import { OutputSink } from "../session/streaming-output";
import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../tools/output-meta";
import { getOrCreateSnapshot } from "../utils/shell-snapshot";
import { loadDirenvEnv } from "./direnv";
import { buildNonInteractiveEnv } from "./non-interactive-env";
export interface BashExecutorOptions {
@@ -215,7 +216,15 @@ export async function executeBash(command: string, options?: BashExecutorOptions
const minimizer = buildMinimizerOptions(settings.getGroup("shellMinimizer"));
const commandCwd = resolveShellCwd(options?.cwd);
const commandEnv = buildNonInteractiveEnv(options?.env);
// Load the repo's direnv/devenv env (cached per .envrc) so devenv tools land
// on PATH; the caller's explicit `env` still wins over direnv-provided values.
const direnvEnv =
settings.get("bash.direnv") === "off"
? null
: await loadDirenvEnv(commandCwd ?? process.cwd(), {
timeoutMs: settings.get("bash.direnvLoadTimeoutMs"),
});
const commandEnv = buildNonInteractiveEnv(direnvEnv ? { ...direnvEnv, ...options?.env } : options?.env);
// Apply command prefix if configured
const prefixedCommand = prefix ? `${prefix} ${command}` : command;