DEV Community

Cover image for Your Agent's Shell Isn't Your Shell: 8 Differences That Break Its Commands
Gabriel Anhaia
Gabriel Anhaia

Posted on Edited on AI-assisted

Your Agent's Shell Isn't Your Shell: 8 Differences That Break Its Commands


You typed nvm use 22 ten minutes ago. Your prompt shows the right Node. Then you ask the agent to run the tests, and it reports a syntax error that only an older Node would throw. Or ll is "not found". Or npm test sits there until it times out.

Nothing is broken. The agent ran your command in a different kind of shell than the one you type into. It is usually non-interactive and often not a login shell. Its output goes to a pipe or a file, not a terminal. Each of those changes which startup files run and how your tools behave.

Eight differences cause most of it. Each one below comes with a command that proves it on your own machine: run it in your terminal, ask the agent to run it too, and compare.

First: which shell is the agent in?

Bash calls a shell interactive when it is "started without non-option arguments (unless -s is specified) and without the -c option, and whose standard input and standard error are both connected to terminals" (bash(1)). An agent that runs bash -c '...' or zsh -c '...' fails that test before it starts.

Two checks tell you what you have. $- lists the shell's option flags, and an interactive shell includes i. test -t 1 is true only when file descriptor 1 is "open and is associated with a terminal" (POSIX test).

echo "$-"
[ -t 1 ] && echo "stdout is a tty" || echo "stdout is not a tty"
Enter fullscreen mode Exit fullscreen mode

Run both yourself, then ask the agent to run them. The difference between the two answers is the rest of this post.

The 8 differences

1. zsh -c never reads your .zshrc

The zsh docs are short on this: .zshenv "is sourced on all invocations of the shell, unless the -f option is set", while .zshrc "is sourced in interactive shells" and .zlogin "is sourced in login shells" (zsh startup files). Every export PATH=... line you put in .zshrc is invisible to a plain zsh -c.

env -i HOME="$HOME" zsh -c 'echo $PATH' \
  | tr ':' '\n' > plain.txt
env -i HOME="$HOME" TERM="$TERM" zsh -ic 'echo $PATH' \
  | tr ':' '\n' > interactive.txt
diff plain.txt interactive.txt
Enter fullscreen mode Exit fullscreen mode

env -i drops the PATH your terminal already exported. Without it, the child zsh inherits that PATH and the diff shows nothing useful. An agent launched from a GUI or a fresh process does not get your terminal's PATH for free. Every line in the diff is a directory your agent may not have.

2. bash -c reads no startup file at all, except $BASH_ENV

An interactive non-login bash "reads and executes commands from ~/.bashrc". A non-interactive one skips that and instead "looks for the variable BASH_ENV in the environment" and runs the file it names (bash(1)). No BASH_ENV, no startup file.

bash -c  'type ll'   # fails: ll is not found
bash -ic 'type ll'   # works if ~/.bashrc defines ll
Enter fullscreen mode Exit fullscreen mode

3. Aliases are off in non-interactive bash, even when you source them

This one surprises people who "fix" item 2 by sourcing .bashrc in a script. The bash manual: "Aliases are not expanded when the shell is not interactive, unless the expand_aliases shell option is set using shopt" (Aliases).

echo 'alias ll="ls -l"' > ./aliases.sh
bash -c 'source ./aliases.sh; ll'   # ll: command not found
bash -c 'shopt -s expand_aliases
source ./aliases.sh
ll'                                  # works
Enter fullscreen mode Exit fullscreen mode

The alias has to be defined on an earlier line than the one that uses it, which is why the working version spans three lines.

Claude Code works around items 1 to 3 for its own Bash tool. Its docs say that at session start it "sources ~/.zshrc, ~/.bashrc, or ~/.profile depending on your shell, captures the resulting aliases, functions, and shell options, and applies them to every Bash command" (Claude Code tools reference). That covers the agent's own commands. It does not cover what those commands start: a make recipe, a git hook, an npm script or a shell script you wrote all get the plain rules above. GNU make, for one, uses /bin/sh unless the makefile says otherwise, and "the variable SHELL is never set from the environment" (GNU make).

A terminal window split in two: the left half full of familiar aliases and paths, the right half nearly empty
Your interactive shell on the left, the agent's -c shell on the right.

4. Claude Code's snapshot is taken once, at session start

A captured environment is a photo. Claude Code takes it "at session start" (same page), so the alias you add to .zshrc mid-session, or the tool you install that edits your PATH, does not reach a session that is already running. Codex also snapshots your shell environment: its features.shell_snapshot setting will "Snapshot shell environment to speed up repeated commands", on by default (Codex config reference). The page doesn't say when that snapshot refreshes, so test it.

Add alias hello='echo hi' to your startup file, ask the running agent to run type hello, then start a fresh session and ask again. If only the fresh one finds it, restart sessions after you change your shell config.

5. export dies with the command

Claude Code's Bash tool "runs each command in a separate process", and "Environment variables don't persist. An export in one command won't be available in the next" (tools reference). The working directory does carry over inside the project, which makes the missing variable more confusing.

Ask the agent to run these as two separate commands:

export API_URL=https://api.example.com
echo "${API_URL:-unset}"
Enter fullscreen mode Exit fullscreen mode

The second prints unset. Put the value in the same command line (API_URL=... npm test), or in a file the agent loads (see the fix below).

Codex adds a second filter: shell_environment_policy.inherit (all, core or none) sets the "Baseline environment inheritance when spawning subprocesses" (Codex config reference). If your team set it to core or none, a variable exported in the terminal you started Codex from may never reach its commands.

6. nvm use changes one shell, and it isn't the agent's

nvm is "a sourced shell function, not an executable binary", "invoked per-shell" (nvm README). Your nvm use 22 edited the PATH of the shell you typed it into. The agent's commands started from somewhere else.

zsh -c 'type nvm'      # not found: .zshrc never ran
nvm alias default 22   # what new shells start with
Enter fullscreen mode Exit fullscreen mode

In Claude Code the nvm function can come along with the snapshot, but item 5 still applies: nvm use 22 in one command changes PATH for that command only, and the next npm test runs with the old one. Set the default alias, so every new shell starts on the version you want.

7. Prompt hooks never fire, because there is no prompt

direnv "needs to be hooked into the shell", and "Before each prompt it checks for the existence of an .envrc file" (direnv(1)). mise's activation is the same idea: "PATH activation does its work at prompts and directory changes" (mise shims docs).

A command the agent runs starts, runs, and exits. It never draws a prompt, so the hook has nothing to fire on, and your .envrc or mise.toml might as well not exist. Both tools ship a one-shot form for exactly this:

direnv exec . npm test
mise exec -- npm test
Enter fullscreen mode Exit fullscreen mode

mise's docs point you the same way: use mise exec or mise run for "CI, scripts, and projects where you do not want to change shell startup files", and shims for programs that "do not start from" the shell you type into.

8. No terminal on stdout, and no job control

Claude Code "streams a command's output to a working file as the command runs" (tools reference). stdout is a file, so every tool that checks for a terminal changes behaviour. git only pipes output into a pager "if standard output is a terminal" (git(1)), which helps here. Colour and progress bars usually switch off. A tool that asks a question waits on stdin for an answer nobody types, until the command timeout kills it (two minutes by default in Claude Code, same page). Pass the tool's non-interactive flag (--yes, -y, --no-input) instead.

Job control is gone too. set -m is "on by default for interactive shells on systems that support it" (bash(1)), and a -c shell isn't one. fg, bg and Ctrl-Z mean nothing there.

bash -c  'echo $-'   # no i, no m
bash -ic 'echo $-'   # i, and m when a terminal is attached
Enter fullscreen mode Exit fullscreen mode

For a dev server, let the agent use its own background mechanism (Claude Code has run_in_background) rather than & and fg.

The fix: put the environment where every shell reads it

Move what the agent needs out of .zshrc and into a place each kind of shell reads.

PATH and plain exports go in ~/.zshenv. It runs for every zsh, interactive or not. Keep it quiet and fast, because it also runs for every script: no echo, no slow commands, nothing that prints.

# ~/.zshenv: read by every zsh, unless -f is set
export PATH="$HOME/.local/bin:$PATH"
export EDITOR=vim
Enter fullscreen mode Exit fullscreen mode

For bash, point BASH_ENV at a small file with the same lines.

Project variables go through direnv or mise, called explicitly. Tell the agent, in your CLAUDE.md or AGENTS.md, to run tests with direnv exec . npm test or mise exec -- npm test.

Commands go in a Makefile. A target fixes the shell and the wrapper once, and the agent only has to remember make test:

SHELL := /bin/bash

test:
    direnv exec . npm test
Enter fullscreen mode Exit fullscreen mode

For Claude Code's own commands, use CLAUDE_ENV_FILE. To make variables persist across Bash commands, the docs say to set CLAUDE_ENV_FILE "to a shell script before launching Claude Code, or use a SessionStart hook to populate it dynamically" (tools reference). The same page says to activate your virtualenv or conda environment before you launch it.

Then rerun the two checks from the top, through the agent, and the eight commands above. They should agree with your terminal on everything except i, m and the tty.

What Hermes changes, and what it doesn't

I build Hermes IDE, so weigh this section with that in mind. Hermes opens every agent, and any plain shell you start, "in a real terminal by default, running the agent's own interface" (README).

That changes one layer. The agent's own interface gets a real terminal, so its prompts, approval dialogs and slash commands behave exactly as the vendor built them. It also means you can open a plain shell in a split pane next to the agent and run the checks from this post side by side.

Two real agent sessions side by side in Hermes IDE, Codex and Claude Code, each in its own worktree
Codex on orders-types and Claude Code on orders-api, each in its own hermes/ worktree, with the sessions sidebar on the left.

It does not change the layer the eight differences live in. When the agent runs a tool command, the agent spawns that process itself, the same way it would in any other terminal. Claude Code still captures its snapshot at start, still drops export between commands, and still sends output to a file. Codex still applies its shell_environment_policy. All eight differences hold inside Hermes. Hermes's own worktree recipe follows the same rules: its setup commands run in sh on macOS and Linux (worktree recipes), and a non-interactive sh won't read your .zshrc either.

Hermes is free to download, source-available under BSL 1.1, and runs on macOS, Windows and Linux. It is not affiliated with Nous Research, Anthropic, OpenAI or any agent vendor. Download it or read the source on GitHub.


If this was useful

A lot of what an agent does in that shell is search: rg, grep, git log -S, reading the files that come back. When plain search stops being enough and you want an agent that retrieves the right chunk of code or docs, the RAG Pocket Guide covers chunking, embeddings and reranking for production. It is one of the 8 short books in the Pocket Guides for Developers series, on agents, RAG, prompts, observability, events, databases and system design: the whole series is here.

RAG Pocket Guide: Retrieval, Chunking, and Reranking Patterns for Production

Top comments (0)