I am still seeing issues with the basic ~/.zshenv setup outlined in the previous comment. I traced it to the direnv export zsh returning empty. This leads to none of the tools installed in the Nix environment pointing to their Nix store locations for the agent.
This seems to stem from conflicts between the Cursor shell snapshot sequence and its interaction with direnv-specific env vars like the DIRENV_DIFF.
It seems like the Cursor agent shell uses the following sequence:
- /bin/zsh starts (inherits env from Cursor extension host)
- zsh sources ~/.zshenv <── YOUR CODE RUNS HER
- zsh executes the -c bootstrap script:
a. snap = read from fd 3
b. eval “$snap” <── SNAPSHOT RESTORES PRIOR STATE
c. eval “$1” <── PRE-TOOL HOOK PREFIX + YOUR COMMAND RUNS HER
- dump_zsh_state to fd 4
The bootstrap script called on each shell invocation for step 3 is called like this:
/bin/zsh -c '<BOOTSTRAP_SCRIPT>' -- '<COMMAND>'
Where the bootstrap script is (traced by asking the agent to print out its environment details):
snap=$(command cat <&3);
builtin unsetopt aliases 2>/dev/null;
builtin unalias -m ‘*’ 2>/dev/null || true;
builtin eval “$snap” && {
builtin unsetopt nounset 2>/dev/null || true;
builtin eval “${__CURSOR_SANDBOX_ENV_RESTORE:-}” 2>/dev/null;
builtin export PWD=“$(builtin pwd)”;
builtin setopt aliases 2>/dev/null;
builtin eval “$1”;
};
COMMAND_EXIT_CODE=$?;
dump_zsh_state >&4;
builtin exit $COMMAND_EXIT_CODE
These direnv env vars, DIRENV_DIFF DIRENV_WATCHES IN_NIX_SHELL, get passed through and maintained in the snapshot, leading to the direnv export not running since the direnv CLI assumes it was already loaded. Attempting to unset those directly in the .zshenv doesn’t work either, since the Cursor snapshot ends up overriding the PATH again after step 2.
For example, this didn’t work:
if [[ -n "$CURSOR_AGENT" ]]; then
unset DIRENV_DIFF DIRENV_WATCHES IN_NIX_SHELL
eval "$(direnv export zsh)"
fi
What does seem to work is a custom pre-tool shell hook:
{
“version”: 1,
“hooks”: {
“preToolUse”: [
{
“command”: “./cursor/hooks/nix-env-shell.sh”,
“matcher”: “Shell”
}
]
}
}
Where the hook command prepends the direnv export right before the command itself:
#!/bin/bash
input=$(cat)
if ! command -v jq >/dev/null 2>&1; then
echo '{"permission": "allow"}'
exit 0
fi
repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
read -r -d '' inject <<INJECT
if [[ "\$PATH" != */nix/store/* ]]; then
unset DIRENV_DIFF DIRENV_WATCHES IN_NIX_SHELL
eval "\$(chpwd_functions=(); cd '$repo_root' && direnv export zsh 2>/dev/null)" || true
fi;
INJECT
echo "$input" | jq --arg prefix "$inject" '{
permission: "allow",
updated_input: {
command: ($prefix + " " + .tool_input.command)
}
}'
The extra “unset” right before the export was critical too, otherwise direnv still runs into the same empty export issue. The PATH check also helps skip additional direnv evaluations once the PATH is correctly updated the first time. As far as I can tell, this works because it defers the direnv evaluation to after the Cursor shell sequence’s internal snapshot restore.