Skip to content

feat(provider): propagate direnv env to agent sessions#138

Merged
Ecko95 merged 2 commits into
gitsfrom
feat/login-shell-env-propagation
Jul 10, 2026
Merged

feat(provider): propagate direnv env to agent sessions#138
Ecko95 merged 2 commits into
gitsfrom
feat/login-shell-env-propagation

Conversation

@Ecko95

@Ecko95 Ecko95 commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Closes #134.

Problem

Agent/provider sessions spawn the provider process directly (not even a non-login shell — a bare execve), so per-project environment managers (direnv .envrc, mise, nix) never hook in. Agents miss the project's toolchain/env vars that work fine in the user's own terminal.

Fix

Resolve the project's direnv environment and merge it into each provider session's child env, below the session-injected vars so #124's GITS_PORT and the git-shim confinement vars always win.

  • packages/shared/src/shell.ts — new resolveDirenvEnvironment(cwd, baseEnv) runs direnv export json and returns the env delta (set/unset), plus applyDirenvDelta. Failure-safe: win32, direnv-not-installed, un-allowed .envrc, timeout, or malformed output all collapse to {} — session start never breaks. It's async (promisify(execFile)) so a slow/hung .envrc never blocks the Node event loop.
  • apps/server/src/provider/direnvSessionEnv.ts — one shared helper (resolveSessionEnvWithDirenv) used by all four adapters.
  • apps/server/src/provider/Layers/{Claude,Codex,Cursor,OpenCode}Adapter.ts — resolve direnv, then git-shim, then merge session vars last.
  • apps/server/src/provider/GitShimManager.tsthe subtle part. The git-shim prepends its dir to PATH so bare git resolves to the confinement shim. It previously built that PATH from the server's own process.env.PATH, and since the shim env is merged last, it clobbered direnv's PATH additions — silently discarding PATH_add, nix/mise/asdf shims (the whole point of the issue). allocate() now takes an optional basePath and prepends the shim dir to the caller's direnv-merged PATH: the shim stays first (confinement intact) and .envrc toolchain dirs survive.

Why not bash -l

The issue floated spawning login shells. Empirically that does not propagate direnv: direnv's PROMPT_COMMAND hook never fires under -lc/-ilc (verified in-sandbox). direnv export json — the same primitive direnv's own shell hook uses — is the correct mechanism, and it respects direnv allow (an un-allowed .envrc exports nothing).

#124 interaction (verified)

GITS_PORT and the confinement vars (GITS_REAL_GIT, GITS_ALLOWED_ROOT, GITS_PROTECTED_BRANCHES, shim-first PATH) are all merged after the direnv delta, so a .envrc cannot override them. The final child PATH is <shimDir>:<direnv additions>:<original PATH>.

Scope

Covers the four provider adapter spawns. The integrated terminal is intentionally not changed — its PTY already sources rc files, so direnv works there via the interactive cd-hook; the login-only gap (/etc/profile → nix) is a narrow follow-up, documented with a ponytail: comment rather than forcing every terminal into login-shell semantics.

Verification

  • Local: forced typecheck 14/14; bunx vitest run on the 6 touched suites → 162/162; oxlint (only pre-existing warnings in the untouched shim-script template) / oxfmt clean.
  • Adversarial review (correctness + security): caught that the git-shim was clobbering direnv's PATH (feature silently ineffective) — fixed as above. Confirmed confinement vars can't be overridden by .envrc, shim dir stays first, direnv allow not bypassed. Added a PATH-survival regression test (CodexAdapter.test.ts) proven to fail against the pre-fix code and pass after.
  • Live e2e (headless server built from this branch, real direnv v2.37.1, Playwright, live Claude Sonnet 4.6 turn): a throwaway project with an allowed .envrc (export GITS_DIRENV_PROBE=itworks_134 + PATH_add toolchain) → the agent session's Bash tool observed:
    • printenv GITS_DIRENV_PROBEitworks_134 (acceptance: env var visible, no manual wrapping) ✅
    • envtoolenvtool-on-path-134 (the PATH_add toolchain reachable — the PATH-clobber fix, live) ✅

Follow-ups (not blocking)

  • Integrated-terminal login-shell flag for /etc/profile-based managers (nix).
  • Explicit mise/nix handling if direnv alone proves insufficient — the resolver's ladder can grow; direnv covers the stated acceptance.

Resolve the project's direnv environment (`direnv export json`) and merge it
into each provider session's child env (Claude/Codex/Cursor/OpenCode), below
the session-injected vars so GITS_PORT (#124) and the git-shim confinement
vars always win. The resolver is async and failure-safe (direnv missing,
un-allowed .envrc, timeout -> no-op), so session start never breaks or blocks.

Fixes the git-shim PATH clobber: allocate() now prepends the shim dir onto the
caller's direnv-merged PATH, so the shim stays first (confinement intact) while
.envrc PATH_add / nix / mise toolchain dirs survive into the child.

Integrated terminal left as-is (its PTY already sources rc files; the
login-only /etc/profile gap is a documented follow-up).

Closes #134

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added size:L vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Jul 9, 2026
@Ecko95
Ecko95 merged commit 6c14009 into gits Jul 10, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant