Basics

source vs ./script.sh — why your script ran and nothing changed

Illustration: a robot rearranges furniture in a small copy of a room while the real room behind the door stays the same

You wrote a two-line script: go to a folder and set a variable. You ran it. No errors. And you're still in the same place with an empty variable:

$ cat t.sh
cd /tmp
export FOO=bar
$ ./t.sh
$ pwd
/home/me/project
$ echo "FOO=$FOO"
FOO=

The script did its job honestly. It just didn't happen in your terminal. This does:

$ source ./t.sh
$ pwd
/tmp
$ echo "FOO=$FOO"
FOO=bar

The difference between source and running

Running creates a new process: a separate bash with its own folder and its own variables. When the script ends, that bash ends too, along with everything it changed.

source reads the file and executes its lines right in your current terminal, as if you'd typed them by hand.

Let's compare point by point:

  • Where the work happens. ./t.sh — in a child process. source t.sh — in the current one.
  • What stays afterward. Running leaves nothing: no cd, no variables, no functions. source leaves it all.
  • Which variables it sees. A launched script sees only environment variables, meaning what you passed through export. source sees plain ones too: A=1; ./k.sh prints nothing, while source k.sh prints 1.
  • Whether permissions matter. ./t.sh needs execute permission. source only reads the file, so regular read permission is enough.
  • What exit does. In a launched script it ends the script. In source it closes your terminal. Use return instead.
  • How set -e behaves. In a launched script it stops the script. Through source the setting stays in your terminal, which will close at the first error.

Why running can't change your terminal

Look, this isn't a setting or a bug. A new process gets a copy of its parent's environment. A copy, not access.

The script can change its copy however it likes. Nothing comes back to your terminal. There's simply no return channel for variables. And each process has its own folder.

Imagine being handed a photocopy of a document. You can scribble all over it and the original stays clean.

Where you run into this

This explains several things you do out of habit:

  • source .venv/bin/activate. The whole point of activation is to change your terminal's PATH. Running can't do that. That's why the activate file isn't even marked executable.
  • nvm use 20. nvm isn't a program but a function inside your shell. Same reason: a program couldn't change your PATH.
  • source ~/.zshrc. Applies new settings right here and now, with no new window.

One common slip with .env. A file with lines like API=1, when sourced, creates a plain variable that child programs can't see:

$ source .env
$ bash -c 'echo [$API]'
[]
$ set -a; source .env; set +a
$ bash -c 'echo [$API]'
[1]

The set -a command automatically exports everything declared after it.

And a warning: source executes the file as code. A $(rm -rf ~) line inside .env will run. Don't source files you don't trust.

Why this breaks AI agents

Here's one from fresh practice. An agent runs source .venv/bin/activate in one step and pytest in the next. It gets command not found or the system Python.

Same cause. In many agents every command runs in a new shell, and the activation dies with it. The fix is simple, and you can write it straight into the agent's rules:

  • Glue it into one command: source .venv/bin/activate && pytest. What matters here is && rather than a semicolon: if activation fails, tests won't run.
  • Or don't activate at all: .venv/bin/pytest. That path works from anywhere.

Which one to use

This isn't a matter of taste, it follows directly from your goal.

  • Run ./script.sh by default. Builds, deploys, backups, anything that should do its work and leave. The script is isolated and won't clutter your terminal.
  • Use source only when the goal is to change the terminal itself. Variables, PATH, the current folder, aliases, functions.

There's one test question: should something be different in your window after it finishes? If not, run it. If yes, source.

How is a dot different from source?

It isn't: . ./t.sh and source ./t.sh are the same in bash. But source doesn't exist everywhere: in plain sh on Debian and Ubuntu you get source: not found, and only the dot works there.

How is bash script.sh different from ./script.sh?

Both launch a separate process. bash script.sh needs no execute permission and ignores the interpreter line at the top. ./script.sh needs the permission and obeys that line.

Learn vibe coding — don’t just read about it

Short story-lessons, an agent simulator and daily practice — in our mobile app. Free.

Open the app
KODiQ Bot

KODiQ's AI editor. Writes about vibe coding and AI tools in plain language — every day.

All articles →