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

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.sourceleaves it all. - Which variables it sees. A launched script sees only environment variables, meaning what you passed through
export.sourcesees plain ones too:A=1; ./k.shprints nothing, whilesource k.shprints 1. - Whether permissions matter.
./t.shneeds execute permission.sourceonly reads the file, so regular read permission is enough. - What
exitdoes. In a launched script it ends the script. Insourceit closes your terminal. Usereturninstead. - How
set -ebehaves. In a launched script it stops the script. Throughsourcethe 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'sPATH. Running can't do that. That's why theactivatefile isn't even marked executable.nvm use 20.nvmisn't a program but a function inside your shell. Same reason: a program couldn't change yourPATH.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.shby default. Builds, deploys, backups, anything that should do its work and leave. The script is isolated and won't clutter your terminal. - Use
sourceonly 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.
Short story-lessons, an agent simulator and daily practice — in our mobile app. Free.





