The Shell Environment: .bashrc, PATH, aliases, and umask

Introduction

When you log into Unity (or any Linux cluster), you don’t just get a generic terminal — you get a personalized environment. Your prompt, your PATH, your aliases, the conda environment that’s active, the umask that controls file permissions: all of these are set by a handful of small files in your home directory that the shell reads on startup.

Most students and new postdocs treat these files as magic. They aren’t. Once you know what they do, you can:

✔ Type getgpu instead of sinteractive -p <group> -g 1 -t 06:00:00 ✔ Type unity on your laptop instead of ssh unity ✔ Have your conda env active automatically on every login ✔ Make sure files you create are readable by your group collaborators by default ✔ Have claude, nvcc, custom Python tools, etc. all just work without typing full paths

This page covers everything you need to make those things happen.

The companion page Persistent Sessions: tmux, nohup, and livenode builds on top of this — once you have a working .bashrc, you can define functions like livenode that survive disconnects.


1. What Is a Shell, Anyway?

A shell is the program that turns what you type into actions. On Unity (and most Linux clusters) the default shell is bash.

When you ssh unity and see a prompt, a fresh bash process started for you. That process reads one or two startup files from your home directory, sets things up, then waits for your commands. When you log out, the process ends — but the files stick around for the next login.

Two important distinctions:

  • Login vs non-login shell: SSH-ing in starts a login shell. Opening a new pane in tmux or VS Code’s integrated terminal sometimes does, sometimes doesn’t.
  • Interactive vs non-interactive shell: A normal SSH session is interactive (it shows you a prompt). A bash my_script.sh invocation is non-interactive.

This matters because different startup files run in different cases (see Section 2).


2. Startup Files: .bash_profile vs .bashrc

These two files live in your home directory (~/.bash_profile and ~/.bashrc). Both are just plain text files full of shell commands.

The rules are slightly weird:

File When it runs
~/.bash_profile When you start a login shell (e.g. you SSH in)
~/.bashrc When you start a non-login interactive shell (e.g. open a new terminal pane inside an existing session)

The annoying part: .bash_profile does not automatically run .bashrc. They’re independent files, and login shells normally only read .bash_profile. So if your .bash_profile doesn’t explicitly source .bashrc, your SSH session won’t see the aliases and functions you put there.

The good news: Unity’s stock .bash_profile already handles this

On Unity, the default ~/.bash_profile you get with a new account contains exactly this block near the top:

# .bash_profile

# Get the aliases and functions
if [ -f ~/.bashrc ]; then
        . ~/.bashrc
fi

# User specific environment and startup programs

PATH=$PATH:$HOME/.local/bin:$HOME/bin

export PATH

The if [ -f ~/.bashrc ]; then . ~/.bashrc; fi block is what makes everything work — every login SSH session sources .bashrc automatically. If your Unity .bash_profile looks like that, you’re fine — just put your stuff in .bashrc.

When you do need to add the line yourself

This is much more common on:

  • macOS — many hand-created ~/.bash_profile files (or ~/.zshrc on newer Macs) don’t source ~/.bashrc
  • Other clusters — some HPC systems’ default skeletons skip the block
  • A .bash_profile you wrote yourself for a custom setup

To check what you actually have:

cat ~/.bash_profile

If you don’t see something equivalent to the Unity stock block above, add this to the bottom:

# Source .bashrc on login so aliases / functions / env vars are always loaded
[ -f ~/.bashrc ] && source ~/.bashrc

(Either form — the if ... fi block or the [ -f ... ] && source one-liner — works the same.)

After this, everything goes in .bashrc and you can forget .bash_profile exists.

Editing the files safely

nano ~/.bashrc

After editing, you can reload it without logging out:

source ~/.bashrc

(Older equivalent: . ~/.bashrc.)

✅ Tip: when you’re experimenting, open two SSH sessions to Unity. Edit in one, source-and-test in the other. If you break your .bashrc so badly you can’t log in, the second session is your lifeline.


3. Aliases — The Single Highest-ROI Thing in Your .bashrc

An alias is a shorthand for a longer command. The syntax is:

alias short='long command goes here'

Once defined, typing short runs the long command.

Examples that pay for themselves on day one

On your laptop’s ~/.bashrc (or ~/.zshrc on macOS):

# So you can just type 'unity' instead of 'ssh unity'
alias unity='ssh unity'

# If you've added a specific compute-node entry to your ~/.ssh/config,
# you can alias that too. Replace `mynode` with the node's actual name.
alias mynode='ssh mynode'

# Forward Jupyter from a compute node to localhost:9099
alias jup='ssh -L 9099:127.0.0.1:8888 mynode'

On the cluster’s ~/.bashrc:

# Grab an interactive node, configured the way you actually want it.
# Replace <group> with your Slurm partition name — for new users this
# is usually `batch` (see Section 4 for how to find out what you can use).
alias getnode='sinteractive -m 48000 -t 48:00:00'
alias getgpu='sinteractive -p <group> -g 1 -t 06:00:00'

# Check your queue without typing your username every time
alias runs='squeue -u $USER'

# A long ls that sorts by modification time, with sizes
alias lst='ls -lhtr'

# Print the absolute, canonical path of the current directory.
# Useful because plain `pwd` can return paths *through* symlinks, which
# look right but cause trouble when fed to scripts or shared with others
# who can't resolve the symlink the same way you can.
alias rpwd='realpath .'

# Open the most recently-modified file in $PWD with `more`
alias more_last='more "$(ls -t | head -n1)"'

# Jump to favorite project directories
# Replace <group> and <username> with your actual values (Section 4).
alias proj='cd /fs/project/<group>/<username>/'
alias data='cd /fs/project/<group>/shared_data/'

After source ~/.bashrc, just type getgpu or proj and the long command runs.

Functions: aliases that take arguments

Aliases can’t take arguments cleanly. When you need them, define a function:

# Show the working directory of one of your Slurm jobs.
# First find a job ID with `squeue -u $USER` (or the `runs` alias above) —
# the output lists each of your running/queued jobs. Then pass the ID:
#   slinfo 5067189        # 5067189 is just an example ID
slinfo() {
    scontrol show jobid -dd "$1" | grep 'WorkDir' | sed 's/WorkDir=//' | sed 's/[[:blank:]]//'
}

5067189 is just an arbitrary placeholder job ID — you’d substitute one you actually got from squeue or runs. The function receives the ID as "$1" at runtime.

Functions are also the right tool for anything that involves conditional logic, multiple commands, or wrapping tmux (see the livenode example in Persistent Sessions).


4. Your Unix Groups and Slurm Partitions

Several of the alias examples above used <group> as a placeholder — e.g. sinteractive -p <group>. That is something you have to replace with your actual group/partition name before the alias will work. But what is a group? And what’s a “partition”? They’re separate-but-related concepts, and both matter on a shared HPC system.

4.1 Unix groups: who can read/edit your files

In Linux, every user belongs to one or more Unix groups. Every file has a single group owner, and the file’s permissions decide what members of that group can read, write, or execute (see Section 7 on umask).

On Unity, your labmates and collaborators are typically gathered into a shared group so you can all access /fs/project/<group>/... directories. The two filesystems you’ll see referenced throughout this handbook are:

Path Purpose
/fs/project/<group>/ Long-term shared storage — lab data, shared environments, code repos. Group-readable when permissions are set right (see Section 7 on umask).
/fs/scratch/ Temporary fast storage — intermediate files during computation. Not backed up; periodically cleaned. Use for things you can regenerate.

To see which Unix groups you belong to:

groups
# example output:  yourname.##  somelab  somelab-students

Or, more verbose:

id
# example output:  uid=12345(yourname.##) gid=12345(yourname.##) groups=12345(yourname.##),5678(somelab)

Each entry is one group you can access files of. Your primary group is the one new files will be created with by default (it’s the first one groups prints).

To see who else is in a given group (e.g. who your labmates are):

getent group somelab
# example output:  somelab:x:5678:alice.1,bob.123,yourname.##

If you have no idea which group(s) you should be in, ask your PI or the project owner — they’ll know the lab’s official group name.

4.2 Slurm partitions: who can use which compute hardware

A Slurm partition is a named pool of compute nodes that Slurm schedules jobs onto. Partitions and Unix groups overlap conceptually but aren’t the same thing — a partition controls compute resource access, not file access.

On Unity, there are typically two flavors of partition:

Partition Who can submit Notes
batch Everyone with a Unity account The free, public partition. Anyone can submit; jobs share the available public hardware via fair-share scheduling. If you’re new and have no idea what to use, use batch.
Lab partitions (named after a lab or PI) Members of that lab’s billing account only Nodes a research group has purchased and reserved for itself. Higher priority for that lab’s members, no access for everyone else.

To list every partition on Unity:

sinfo                                  # one-line summary per partition
sinfo -o "%P %a %D %N"                 # partition name, state, node count, node list

To see which partitions you are authorized to submit to:

sacctmgr show association user=$USER format=Account,Partition

If a partition doesn’t appear in that output, you can’t submit jobs to it. To get added to a lab partition, request access through your PI or the cluster admins.

4.3 What <group> means in the alias examples

When you see <group> in sinteractive -p <group> in this handbook, it means the name of a Slurm partition you have access to. For most readers that will literally be batch:

alias getgpu='sinteractive -p batch -g 1 -t 06:00:00'

Once you’re added to a lab partition, switch the -p value to that lab partition’s name (e.g. -p howat) for higher priority and access to the lab’s reserved nodes.

The same <group> placeholder also appears in /fs/project/<group>/... paths — that’s the Unix group name for the shared lab directory, which your PI will tell you.

The Slurm Basics and Slurm Best Practices pages cover partitions, accounts, and job submission in much more depth.


5. Environment Variables and PATH

An environment variable is a name-value pair that programs running inside your shell can see.

echo $HOME             # /home/yourname.##
echo $USER             # yourname.##
echo $PATH             # a long colon-separated list

Setting one:

MY_VAR="hello"

This sets it for your shell only. To make it visible to programs you launch:

export MY_VAR="hello"

The export keyword is the whole reason this is its own concept — without it, child processes don’t inherit the variable.

The PATH variable

PATH is a colon-separated list of directories that the shell searches when you type a command:

echo $PATH
# /home/yourname.##/miniconda3/bin:/usr/local/bin:/usr/bin:/bin

When you type python, bash walks the list left to right and runs the first python it finds. So order matters — the directory listed first wins.

Adding a directory to PATH

The standard pattern:

export PATH=/path/to/my/tools:$PATH

This prepends the new directory, so your tool wins over any system version.

To append (lower priority):

export PATH=$PATH:/path/to/my/tools

Other common environment variables on HPC

Variable Purpose
LD_LIBRARY_PATH Where the dynamic linker looks for shared libraries (.so files). Often needed when running self-compiled code
CUDA_HOME Where the CUDA toolkit is installed
ANTHROPIC_API_KEY, GITHUB_TOKEN, etc. Used by AI coding assistants (Claude Code, GitHub Copilot CLI, etc.) — see warning below
OMP_NUM_THREADS OpenMP thread count for multithreaded code
TMPDIR Where temp files go — Unity typically wants this pointed at a fast scratch filesystem
umask Default file/directory permissions (Section 7)

⚠ A note on secrets in .bashrc

It’s common to put API keys in .bashrc:

export ANTHROPIC_API_KEY="sk-ant-..."

This works, but be aware:

  • chmod 644 ~/.bashrc would expose the key to other users on the cluster
  • ❌ Checking your .bashrc into a git repo (even by accident) would publish the key
  • ❌ Sharing your home dir with a backup tool that uploads anywhere will leak it

Mitigations:

chmod 600 ~/.bashrc                      # only you can read it
ls -l ~/.bashrc                          # confirm

For maximum safety, store sensitive tokens in a separate file you source only when needed, and never commit that file:

# In ~/.bashrc:
[ -f ~/.secrets ] && source ~/.secrets   # only your shell loads it

# In ~/.secrets (chmod 600):
export ANTHROPIC_API_KEY="sk-ant-..."

If you ever notice a secret has leaked, rotate it immediately — assume it’s compromised.


6. module load: The Cluster’s Software Toolkit

HPC clusters host many versions of compilers, libraries, and applications simultaneously — Python 3.9 and Python 3.11, multiple CUDA toolkits, several Intel compiler releases, etc. Hard-coding paths to all of these would be a mess.

The Lmod modules system solves this. Inside your .bashrc (or interactively at the prompt):

module avail                 # what's available?
module avail cuda            # filter to just cuda modules
module load intel/2024.2.0   # set PATH/LD_LIBRARY_PATH/etc. for this Intel compiler
module list                  # what's currently loaded?
module purge                 # unload everything

When you module load X, the system silently appends to your PATH and LD_LIBRARY_PATH and sets a few environment variables. It’s just a fancy way of setting environment variables — there’s no magic.

If you always want a specific module on every login, put the module load line in your .bashrc:

module load intel/2024.2.0

7. umask: Default Permissions for New Files

Every file you create gets a set of permissions decided by your umask. The umask is a subtractive mask — it tells the OS which permission bits to strip from new files.

umask value New file permissions New directory permissions Meaning
022 (default on most systems) rw-r--r-- (644) rwxr-xr-x (755) Owner can write, others can read
002 rw-rw-r-- (664) rwxrwxr-x (775) Group can write too
077 rw------- (600) rwx------ (700) Only owner can read

✅ Why umask 002 is important for collaborative groups

On HPC clusters, lab members often share /fs/project/<group>/... directories. With the default 022 umask:

  • You write a file → permissions are rw-r--r--
  • Your collaborator tries to edit it → ❌ permission denied
  • You have to manually chmod g+w file after every save
  • Conda envs you create can’t be activated by labmates

With umask 002:

  • You write a file → permissions are rw-rw-r--
  • Your collaborator has the same Unix group → ✅ they can edit it
  • Conda envs you create in a shared project dir → ✅ everyone in the group can activate them
  • No chmod ceremony

To enable it, add to your ~/.bashrc:

umask 002

This is strongly recommended for any user working in a shared lab directory.

What about the “world” (other) bits?

umask 002 leaves r-- for the world, which means anyone with a Unity account can read your files. That’s usually fine on a trusted research cluster, but if you have genuinely sensitive data, use umask 027 (group can read, world can’t see at all) or store it under a directory chmodded 700.


8. The Conda/Mamba Initialization Block

After you (or the cluster) install Miniconda or Mambaforge, an automatic block gets added to your .bashrc that looks like this:

# >>> conda initialize >>>
# !! Contents within this block are managed by 'conda init' !!
__conda_setup="$('/home/yourname.##/mambaforge/bin/conda' 'shell.bash' 'hook' 2> /dev/null)"
if [ $? -eq 0 ]; then
    eval "$__conda_setup"
else
    if [ -f "/home/yourname.##/mambaforge/etc/profile.d/conda.sh" ]; then
        . "/home/yourname.##/mambaforge/etc/profile.d/conda.sh"
    else
        export PATH="/home/yourname.##/mambaforge/bin:$PATH"
    fi
fi
unset __conda_setup

if [ -f "/home/yourname.##/mambaforge/etc/profile.d/mamba.sh" ]; then
    . "/home/yourname.##/mambaforge/etc/profile.d/mamba.sh"
fi
# <<< conda initialize <<<

Don’t delete this. It’s what makes conda activate and mamba activate work. The block adds conda’s shell functions, sets up your prompt, and puts the conda command on your PATH.

If you want a specific environment active on every login, add this after the block:

mamba activate myproject

(Or conda activate — either works.)

See the Conda Environments page for the bigger picture on environments.


9. Putting It All Together: An Annotated Example .bashrc

This is a realistic, sanitized example showing common patterns. Replace <username> and <group> with your own values (see Section 4 if you don’t know what those should be), and do not paste tokens straight into a .bashrc — see Section 5.

# ───────────────────────────────────────────────────────────────────────
# Source global definitions (system-wide bashrc)
# ───────────────────────────────────────────────────────────────────────
if [ -f /etc/bashrc ]; then
    . /etc/bashrc
fi

# ───────────────────────────────────────────────────────────────────────
# Default permissions: group can read+write files I create.
# Critical for shared lab directories on Unity.
# ───────────────────────────────────────────────────────────────────────
umask 002

# ───────────────────────────────────────────────────────────────────────
# PATH: prepend my project tools so they win over system versions
# ───────────────────────────────────────────────────────────────────────
export PATH=/fs/project/<group>/<username>/Codes:$PATH
export PATH=/home/<username>/local/bin:$PATH

# ───────────────────────────────────────────────────────────────────────
# Library paths for self-compiled code (CUDA, custom libs)
# ───────────────────────────────────────────────────────────────────────
export CUDA_HOME=/usr/local/cuda/12.2/
export LD_LIBRARY_PATH=/usr/local/cuda/12.2/lib64:$LD_LIBRARY_PATH

# ───────────────────────────────────────────────────────────────────────
# Modules (compilers, libraries) loaded on every login
# ───────────────────────────────────────────────────────────────────────
module load intel/2024.2.0

# ───────────────────────────────────────────────────────────────────────
# Aliases: short names for things I do all the time
# ───────────────────────────────────────────────────────────────────────
# Interactive job allocation. Replace <group> with your partition
# (often `batch` for new users — see Section 4).
alias getnode='sinteractive -m 48000 -t 48:00:00'
alias getgpu='sinteractive -p <group> -g 1 -t 06:00:00'

# Check my queue / lab's queue:
alias runs='squeue -u $USER'

# Friendlier ls:
alias lst='ls -lhtr'

# Jump to project directories:
alias proj='cd /fs/project/<group>/<username>/'
alias data='cd /fs/project/<group>/shared_data/'

# Mac-equivalent name for an editor:
alias pico='nano'

# Open the most recently modified file:
alias more_last='more "$(ls -t | head -n1)"'

# ───────────────────────────────────────────────────────────────────────
# Functions: like aliases, but they take arguments
# ───────────────────────────────────────────────────────────────────────
# Print the working directory of a Slurm job ID
slinfo() {
    scontrol show jobid -dd "$1" | grep 'WorkDir' | sed 's/WorkDir=//' | sed 's/[[:blank:]]//'
}

# ───────────────────────────────────────────────────────────────────────
# Conda/Mamba initialization (don't touch — managed by `conda init`)
# ───────────────────────────────────────────────────────────────────────
# (block omitted for brevity — see Section 8)

# ───────────────────────────────────────────────────────────────────────
# Activate the conda environment I use most often, on every login
# ───────────────────────────────────────────────────────────────────────
mamba activate myproject

# ───────────────────────────────────────────────────────────────────────
# Secrets: kept in a separate file so I don't accidentally commit them
# ───────────────────────────────────────────────────────────────────────
[ -f ~/.secrets ] && source ~/.secrets

# ───────────────────────────────────────────────────────────────────────
# Prevent core dumps (large files from crashed programs)
# ───────────────────────────────────────────────────────────────────────
ulimit -S -c 0

# ───────────────────────────────────────────────────────────────────────
# Misc tweaks
# ───────────────────────────────────────────────────────────────────────
export FORT_FMT_RECL=2000000      # large formatted Fortran I/O record length
export OMP_NUM_THREADS=4          # default OpenMP thread count

After editing, source ~/.bashrc to apply.


10. Common Pitfalls

❌ Aliases don’t appear in scripts

Aliases only work in interactive shells. Inside a #!/bin/bash script, getgpu won’t resolve. Use the full command, or define a function instead (functions are exported via export -f).

❌ “command not found” — but I just defined it

You need to either restart the shell or source ~/.bashrc. Editing the file alone has no effect on running shells.

PATH change broke my shell

If you accidentally wrote export PATH=/some/path (without :$PATH), you removed every standard directory from PATH and now nothing works. Recovery:

export PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin
nano ~/.bashrc        # fix the line

❌ “I added it to .bash_profile and it doesn’t work in VS Code’s terminal”

VS Code’s integrated terminal often starts a non-login shell — which reads .bashrc, not .bash_profile. Make sure .bash_profile sources .bashrc (Section 2).

❌ Newly-created files are still 644 instead of 664

Either:

  • umask 002 is set but you’re in a directory not owned by your collaborative group — check ls -ld .
  • A different umask line later in your .bashrc overrode it — check with umask at the prompt

❌ Conda environment isn’t active in scripts

Slurm batch jobs source a stripped-down shell. Inside myjob.slurm, explicitly activate:

source ~/.bashrc          # or: source ~/mambaforge/etc/profile.d/conda.sh
mamba activate myproject

11. Summary

  • ✔ Put aliases, functions, and environment variables in ~/.bashrc
  • ✔ Make sure ~/.bash_profile sources .bashrc so login shells see everything
  • ✔ Use alias for shorthand, functions when you need arguments
  • ✔ Prepend to PATH for project tools; append for lower priority
  • ✔ Set umask 002 for collaborative group work — saves endless chmod ceremony
  • ✔ Don’t paste secrets straight into .bashrc; use a chmod-600 ~/.secrets file
  • source ~/.bashrc to reload after edits

Next up: Persistent Sessions: tmux, nohup, and livenode — using your shell environment to build sessions that survive disconnects.