Skip to main content

Command Palette

Search for a command to run...

I Quit .bashrc.

Decomposing My Shell Environment into Three Layers

Updated
5 min read
I Quit .bashrc.

Introduction

One morning, the j command was gone.

I had been using it as an alias for zoxide, but suddenly it returned command not found. It worked fine in Ghostty, but not in VSCode's integrated terminal. Zed behaved differently yet again.

I had built the perfect "I can't explain why this works" environment.

This is the story of what happens when you keep piling settings into .bashrc — and what you gain when you stop.


Before: The Everything-in-.bashrc Era

When I started Linux development, I added a new line to .bashrc every time I introduced a useful tool.

# .bashrc (bloated state)
eval "$(starship init bash)"
eval "$(zoxide init bash)"
eval "$(fzf --bash)"
eval "$(keychain --quiet --eval --agents ssh id_ed25519_work id_ed25519_personal)"
source ~/.bash_aliases
export PATH="\(HOME/.local/bin:\)PATH"
# ... secrets, completions, etc.

Aliases, completions, environment variables, and tool initializations were all jammed into a single file.

This caused three main problems.

① PATH inconsistency

Some tools would have their paths registered and some wouldn't. Every time I opened a new terminal, things might behave differently.

② Behavioral differences across terminals

Commands that worked in Ghostty didn't work in VSCode's integrated terminal. The disappearing j command was the clearest symptom.

③ Loss of reproducibility

I could no longer explain why things worked. Was it my config? Something I ran earlier? I had no idea.


The Insight: Shell ≠ Environment

As I worked through the problem, something clicked.

Shell ≠ Environment Shell = UI (the interface) Environment = execution context

.bashrc had been doing two completely different jobs at once: configuring the OS-standard shell and initializing the development environment. That was the root of all the confusion.

The problem wasn't the tools themselves (zoxide, starship). It was the mixing of initialization paths. Login shells and interactive shells read different files — that discrepancy was causing the behavioral differences across terminals.


After: Decomposing into Three Layers

Based on this insight, I split the environment into three distinct layers.

bash     = OS-standard / preservation layer
zsh      = development environment layer
terminal = display layer (all unified to zsh -l)

Returning bash to a preservation layer

I reset .bashrc to the system default.

cp /etc/skel/.bashrc ~/.bashrc

The only thing I kept was an expanded HISTSIZE. All development-specific configuration was removed.

Consolidating the development environment into zsh

Development tool initialization moved to .zshrc.

# ~/.zshrc
eval "$(starship init zsh)"
eval "$(zoxide init zsh)"
source ~/.zsh_aliases
export PATH="\(HOME/.local/bin:\)PATH"

Aliases were also separated into .zsh_aliases.

Unifying all terminals to zsh -l

I updated each terminal's configuration to launch zsh as a login shell.

Ghostty

command = /usr/bin/zsh -l

VSCode

"terminal.integrated.profiles.linux": {
  "zsh": {
    "path": "/usr/bin/zsh",
    "args": ["-l"]
  }
},
"terminal.integrated.defaultProfile.linux": "zsh"

Zed (v1.0.0)

"terminal": {
  "shell": {
    "with_arguments": {
      "program": "/usr/bin/zsh",
      "args": ["-l"]
    }
  }
}

WezTerm (~/.wezterm.lua)

local wezterm = require 'wezterm'
local config = {
  default_prog = { '/usr/bin/zsh', '-l' },
  font = wezterm.font('JetBrainsMono Nerd Font'),
  font_size = 13.0,
  color_scheme = "Dracula",
  enable_tab_bar = false,
}
return config

WezTerm takes the shell and its arguments as an array via default_prog. Font settings can be unified here as well.


Gotchas Along the Way

A few things tripped me up during the migration.

Icons turn into tofu squares in VSCode only

Symptoms:

Ghostty → OK
Zed     → OK
VSCode  → icons render as □ (tofu)

The cause: VSCode has a separate font setting for its integrated terminal.

"terminal.integrated.fontFamily": "JetBrainsMono Nerd Font Mono"

Adding this line fixed it.

The display name and actual name of the font don't match

Display name: JetBrainsMono Nerd Font Mono
Actual name:  JetBrainsMono NFM

Ghostty resolves this transparently via fontconfig. WezTerm and Zed may require specifying JetBrainsMono NFM explicitly.

The j command disappeared

I had alias j='z' in .bash_aliases, but after migrating to zsh it needed to move to .zsh_aliases.

# ~/.zsh_aliases
alias j='z'
alias ji='zi'

\(0 and \)SHELL show different values

echo $0      # → zsh
echo $SHELL  # → /bin/bash

$SHELL reflects the login shell, not necessarily the currently running shell. Adding this to .zshrc aligns them:

export SHELL=/usr/bin/zsh

Results

Before: everything in .bashrc → non-deterministic environment
After:  bash=preservation / zsh=development / terminal=display → determinism restored
  • Same environment regardless of which terminal launches it
  • PATH issues eliminated
  • Debugging scope narrowed to the application itself

The biggest win: I can now explain why things work.


Closing

Quitting .bashrc wasn't about abandoning tools. It was about making ownership of the environment explicit.

Shell is UI. Environment is execution context. Cramming both into the same file was the source of all the confusion.

In the next post, I'll cover what happened immediately after this migration: Obsidian Git's SSH authentication broke, and I found myself in a battle with ssh-agent and the Flatpak sandbox.