Skip to content

feat(server): agents start with the project's direnv environment - #13739

Closed
otavio wants to merge 3 commits into
pingdotgg:mainfrom
otavio:t3code/support-direnv-agent-environment
Closed

otavio wants to merge 3 commits into
pingdotgg:mainfrom
otavio:t3code/support-direnv-agent-environment

Conversation

@otavio

@otavio otavio commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

What Changed

Provider sessions now load the direnv environment (.envrc) of their workspace, the way a shell with the direnv hook does. This applies to every provider: Codex, Claude, Cursor, Grok, OpenCode and Antigravity.

  • Trust stays with direnv. Only .envrc files approved with direnv allow are loaded. A blocked, failing or timed-out one shows as a thread warning, and the agent starts without it.
  • Checked before every message. On a reused session, the server runs direnv's own staleness check against the environment the session started with. If nothing changed, it only compares file timestamps and takes milliseconds. If the user allowed or edited .envrc, or a watched file such as flake.lock changed, the session restarts from its saved state and picks up the new environment, so no new thread is needed.
  • One evaluation per checkout. Threads that start together in the same checkout share a single evaluation, because a first nix develop can take minutes.
  • Allow .envrc. A blocked-.envrc warning has this button on web and mobile. It calls a new projectEnvironment.allowDirenv RPC with the orchestration:operate scope. The server resolves the .envrc from the thread, so a client cannot allow an arbitrary path.
  • Setting. "Load direnv environment" is on by default and can be overridden per environment or per project (Settings → General on web, Agent behavior on mobile).
  • Precedence. Variables set on the provider instance, such as CODEX_HOME or CLAUDE_CONFIG_DIR, win over the .envrc.
  • Docs. A "Project environments with direnv" section in docs/user/project-settings.md.

Why

Projects that provide their toolchain through direnv (Nix dev shells, devenv, mise) only worked in integrated terminals. Agents never run through a shell, so they started without the project's tools, compilers and pinned runtimes.

UI Changes

A blocked .envrc warning in the thread timeline gets an Allow .envrc button. After you click it, the button reads "Allowed · applies to your next message". Dev-server run against a demo project:

Blocked .envrc warning, Allow, and the next answer loading the environment

What the screenshot shows:

  1. The first answer is NOT SET: the .envrc was explicitly denied, which the server honours silently.
  2. The .envrc was edited, so direnv no longer trusts it. The per-message check picked this up and posted the blocked warning with the button.
  3. After Allow, the next message restarted the session, and the agent read loaded from .envrc.

Warnings with an action stay on their own row instead of folding into a collapsed work group (web and mobile), and the button keeps its state when the timeline remounts the row.

This PR also adds a "Load direnv environment" toggle in Settings → General (web) and Agent behavior (mobile). Screenshots of the toggle and of mobile are still to come.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes (timeline done; settings and mobile pending)
  • I included a video for animation/interaction changes

Verification

  • The affected suites pass for server, contracts, client-runtime and web (including the full server.test.ts WebSocket suite).
  • contracts, client-runtime, web, mobile and server typecheck.
  • Checked against the real direnv binary: blocked, then allow, then load, then an unchanged check. Editing the .envrc blocks it again, and after a new allow the check detects the change and loads the new values.

Not included

There is no "Loading project environment…" status while a slow evaluation runs; the thread shows the generic working timer. Adding that needs a progress field or activity that nothing carries today, so it is left for a separate PR.


Done with Claude Opus 5.5 in Claude Code (via T3 Code).

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Sep 26, 2026
Projects that provide their toolchain through direnv (Nix dev shells, devenv, mise) only worked in
integrated terminals: agents never go through a shell, so they ran without the project's tools.

Provider sessions now load the `.envrc` governing their workspace, as a shell with the direnv hook
would, for every provider. Trust stays with direnv: only `.envrc` files approved with
`direnv allow` are loaded, and a blocked or failing one surfaces as a thread warning while the
agent starts without it.

Before each message on a reused session, the server runs direnv's own staleness check against the
environment the session started with. It is cheap when nothing changed, and when the user allowed
or edited the `.envrc` or a watched file such as `flake.lock` changed, the session resumes with the
new environment instead of requiring a new thread. Concurrent loads of one checkout share a single
evaluation, since a first `nix develop` can take minutes.

A blocked `.envrc` warning offers "Allow .envrc" on web and mobile, backed by a
`projectEnvironment.allowDirenv` RPC that resolves the path from the thread. The behavior is on by
default and can be turned off per environment or project with "Load direnv environment".
Testing the draft in a real client showed two problems. The timeline folded a lone warning into a
collapsed work group on web and mobile, which hid its "Allow .envrc" button behind a click; warnings
that offer an action now stay on their own row, like errors and answered questions do.

Sessions also restart for reasons unrelated to the environment (an interrupted turn, a Claude model
selection), and each restart re-published the same blocked-.envrc warning. Session start now skips a
failure identical to the one the thread already shows, as the per-message check did.
…emounts

In a real client the button read "Allowed" directly above the agent's earlier "NOT SET", which
looked like a failure, and it reverted to "Allow .envrc" whenever the virtualized timeline remounted
the row. The label now says the environment applies to the next message, and the outcome is kept per
warning so it survives remounts on web and mobile.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant