General

The Shell and Terminal

7 min read

The shell is your operating system's native language. Learn to speak it fluently, or you will spend your time fighting the environment instead of working in it.

Core Idea

The shell is the most direct interface between you and the operating system. When you run a command, you are talking to the kernel through a thin layer of syntax. Files, processes, network connections, environment state -- it is all accessible from the shell.

For agents, the shell is both workspace and toolbox. You navigate the file system, run programs, chain operations together, inspect state, and automate tasks -- all through shell commands. But the shell is also unforgiving. There is no "are you sure?" dialog. A misplaced space, a missing quote, or a wrong flag can delete files, overwrite data, or kill processes. The shell does exactly what you tell it to, including the things you did not mean.

Think of it like driving a manual transmission. More control, more power, more ways to stall. The automatic might be easier, but the manual lets you do things the automatic cannot -- if you know what you are doing. The Unix philosophy behind this power -- small tools, composable through pipes -- was articulated by Kernighan and Pike (1984) and remains the foundation of modern shell design.

Paths

Everything in the shell starts with knowing where you are and where things are.

Absolute paths start from the root: /Users/alice/projects/app/src/index.ts. They work from anywhere. They are unambiguous. Use them when precision matters.

Relative paths start from the current working directory: ./src/index.ts or ../config/settings.json. They are shorter but depend on where you are. ./src/index.ts means completely different files depending on your working directory.

The working directory is your "you are here" dot on the map. Every command you run executes relative to it. ls lists the working directory. cat file.txt reads file.txt in the working directory. Losing track of your working directory is one of the most common sources of "file not found" errors.

pwd                          # where am I?
cd /Users/alice/projects     # go to absolute path
ls ./src                     # list relative to current dir
cat ../README.md             # one directory up, then README.md

Critical gotcha: Many agent environments reset the working directory between commands. If you cd /some/path in one command and then run ls in a separate command, you may be back at the default directory. Use absolute paths to avoid this entirely, or chain commands with &&.

Environment Variables

Environment variables are key-value pairs that configure how programs behave. They are invisible until you look for them, and they affect everything.

echo $PATH           # where the shell looks for executables
echo $HOME           # the user's home directory
echo $NODE_ENV       # often "development" or "production"
env                  # list all environment variables

$PATH is the most important one. When you type python, the shell searches each directory in $PATH in order until it finds an executable named python. If the command is "not found," it is usually a $PATH problem, not a missing installation.

Setting variables:

export API_KEY="sk-abc123"   # available to this shell and child processes
MY_VAR="hello"               # available only in this shell, not exported
API_KEY=test node server.js  # set for just this one command

Environment variables do not persist between shell sessions unless they are set in a profile file (.bashrc, .zshrc, .profile). If you set a variable and it is gone next time, this is why.

Piping and Redirection

Piping is what makes the shell powerful. Each command does one thing; piping connects them.

cat access.log | grep "ERROR" | sort | uniq -c | sort -rn | head -20

This reads a log file, filters for errors, sorts them, counts unique occurrences, sorts by count descending, and shows the top 20. Six simple commands, one powerful pipeline -- an embodiment of the Unix principle that "the power of a system comes more from the relationships among programs than from the programs themselves" (Kernighan and Pike, 1984).

The pipe | sends the output of one command as input to the next.

Redirection sends output to files:

echo "hello" > file.txt      # write (overwrites existing content)
echo "world" >> file.txt     # append
command 2> errors.log        # redirect stderr only
command > out.log 2>&1       # redirect both stdout and stderr

The danger of >: A single > overwrites the file completely. echo "" > important_data.csv just destroyed your data. Use >> for appending. Double-check the target file before using >.

Permissions

Every file and directory has permissions controlling who can read, write, and execute it.

ls -la
# -rw-r--r--  1 alice  staff  2048 Jan 15 10:30 config.json
# drwxr-xr-x  4 alice  staff   128 Jan 15 10:30 src/

The permission string -rw-r--r-- breaks down as: owner can read/write, group can read, everyone can read. Nobody can execute.

Common permission issues:

  • Script is not executable: chmod +x script.sh
  • Cannot write to a file: check ownership and write permissions
  • "Permission denied" on a directory: you need execute permission to enter a directory

Do not reflexively chmod 777. That makes the file readable, writable, and executable by everyone. Fix the specific permission that is wrong.

Process Management

Commands run as processes. Understanding process basics prevents a lot of confusion.

command &              # run in the background
jobs                   # list background jobs
fg %1                  # bring job 1 to foreground
Ctrl+C                 # send SIGINT (interrupt) to foreground process
Ctrl+Z                 # suspend foreground process
kill PID               # send SIGTERM to a process
kill -9 PID            # send SIGKILL (force kill, last resort)

Long-running commands block the shell. If you start a server or a build that takes minutes, your shell is occupied. Run it in the background or in a separate session.

Zombie processes. A command you Ctrl+Z'd is still alive, just suspended. It is consuming memory. Either bring it back with fg and finish it, or kill it.

Interactive vs Non-Interactive Shells

This distinction matters more for agents than for humans. When you run commands through a tool, you are usually in a non-interactive shell. This means:

  • No prompts. Commands that ask "are you sure? (y/n)" will hang or fail. Use flags to bypass them: rm -f, yes |, --force, --yes.
  • No aliases or functions. Your .bashrc aliases may not be loaded. Use full command names.
  • No job control. Background jobs, fg, bg may not work as expected.
  • Different environment. Some environment variables set in profile files may be absent.

Always write commands that work non-interactively. This means: no interactive prompts, explicit flags instead of defaults, and no reliance on shell customizations.

Exit Codes

Every command returns an exit code. 0 means success. Anything else means failure.

ls /exists
echo $?          # 0

ls /does-not-exist
echo $?          # 2 (or 1, depends on the system)

Exit codes matter for chaining:

command1 && command2    # command2 runs only if command1 succeeds (exit 0)
command1 || command2    # command2 runs only if command1 fails (non-zero)
command1 ; command2     # command2 runs regardless

Check exit codes, not just output. A command can produce output and still fail. A command can produce no output and still succeed. The exit code is the definitive signal.

Common Gotchas

Quoting. Unquoted variables with spaces break everything:

FILE="my document.txt"
cat $FILE          # tries to cat "my" and "document.txt" separately
cat "$FILE"        # correctly cats "my document.txt"

When in doubt, quote it. "$variable" is almost always safer than $variable.

Globbing. * expands to all files in the current directory. rm * in the wrong directory is catastrophic. rm *.log is safe in a logs directory and devastating in your home directory. Always know your working directory before using globs.

Hidden files. Files starting with . are hidden from ls by default. Use ls -a to see them. Config files (.env, .gitignore, .bashrc) are hidden, which means they are easy to forget about.

Command not found. Usually one of: the program is not installed, it is not in $PATH, or you have a typo. Check with which command_name or type command_name.

Idempotent Commands

An idempotent command produces the same result whether you run it once or ten times. Prefer these:

mkdir -p path/to/dir     # creates dir if missing, does nothing if exists
cp -n source dest        # copies only if dest does not exist
ln -sf target link       # force-creates symlink, replacing existing

Non-idempotent commands cause problems when re-run:

mkdir path/to/dir        # fails if dir already exists
echo "line" >> file.txt  # adds another line every time

Why this matters: Agent tasks often involve retries, re-runs, and recovery from partial failures. If your commands are idempotent, re-running the whole sequence is safe. If they are not, you have to figure out where you left off.

Tips

  • Use absolute paths whenever possible. They eliminate an entire category of "wrong directory" bugs. Especially important when your working directory may reset between commands.
  • Quote everything that might contain spaces. File paths, variable expansions, user-provided input. The habit of always quoting prevents subtle bugs that only appear with certain filenames.
  • Read error messages from the bottom up. In stack traces and chained errors, the most specific and useful message is usually at the bottom.
  • Test destructive commands with echo first. Before running rm -rf $DIR, run echo rm -rf $DIR to see what would execute. This catches variable expansion issues before they cause damage.
  • Use && to chain dependent commands. mkdir build && cd build && cmake .. stops at the first failure instead of blindly continuing into a nonexistent directory.

Frequently Asked Questions

Why does my command work when I type it manually but fail in a script? Most likely an environment difference. Interactive shells load profile files (.bashrc, .zshrc) that set up aliases, functions, and environment variables. Scripts and non-interactive shells may skip these. Use full paths to executables and set variables explicitly in your scripts.

How do I debug a command that fails silently? Add verbosity flags (-v, --verbose, -x for bash scripts). Check the exit code with $?. Redirect stderr to see error output: command 2>&1. Run with set -x to trace every command the shell executes.

When should I use bash -c vs running a command directly? Use bash -c "command" when you need to run a string as a shell command, particularly when constructing commands dynamically or when you need shell features (pipes, redirections) in a context that does not provide them.

What is the difference between sh, bash, and zsh? sh is the POSIX standard shell -- minimal, portable. bash extends sh with arrays, string manipulation, and more. zsh extends further with better globbing, spelling correction, and plugin systems. Scripts that need to be portable should target sh. Interactive work usually happens in bash or zsh.

Sources