Bash Configuration
This dotfiles repository includes several bash-related configurations and tools.
Bash Startup Files
The bash configuration is organized as follows:
~/.bashrc: Main bash configuration file~/.bashrc.d/*.sh: Modular bash configuration files~/.profile: Login shell profile
Non-Interactive Short-Circuit
~/.bashrc aborts on its first line for non-interactive shells, so none of ~/.bashrc.d/*.sh runs.
Interactive vs non-interactive
These describe whether the shell reads commands from a prompt, independently of whether it sources ~/.bashrc:
- Interactive — a prompt you type at (
$-containsi). E.g.ssh hostwith no command. - Non-interactive — handed a one-off command and exits (no
i). E.g.ssh host '<cmd>', or the remote shells thatrsync/scpspawn.
The guard keys off this property ($-), not off whether ~/.bashrc was sourced.
The sshd abnormality
Normally a non-interactive bash does not read ~/.bashrc at all. The exception: when bash detects it was started by sshd it sources ~/.bashrceven though it is non-interactive (a compile-time feature enabled in practically every build). So rsync/scp/ssh host '<cmd>' are non-interactive — line editing is off, so bind warns — yet they abnormally run ~/.bashrc. Any stdout written during that sourcing corrupts the program's stream; Atuin's init emitting a bind warning broke rsync:
$ rsync -av some_file.txt remote-dotfiles-host:/home/user/dir
.bashrc.d/30-atuin.sh: line 747: bind: warning: line editing not enabled
protocol version mismatch -- is your shell clean?The guard keeps the stream clean and makes these one-off shells start instantly.
Limitation
Skipping ~/.bashrc.d/ means tooling activated there (mise, nix, homebrew) is unavailable to non-interactive remote commands, so ssh host 'mise-managed-tool …' won't find those tools. Anything that must survive into non-interactive sessions belongs in ~/.profile / ~/.profile.d/*.sh; interactive-only setup stays in ~/.bashrc.d/.
This is an acceptable trade-off because the common non-interactive path (rsync/scp) is pure file transfer and needs no tooling. If the limitation ever becomes a blocker — e.g. you routinely run ssh host '<managed-tool> …' — drop the top-of-file guard and instead gate only the offenders per-script (the bind calls in 30-atuin.sh, the bare where at the end of ~/.bashrc). Tooling then loads non-interactively, at the cost of full startup on every remote command and ongoing vigilance against new stdout offenders.
chezmoi run scripts: re-establish PATH yourself
The same short-circuit hits chezmoi's run_* scripts under home/.chezmoiscripts/: they execute as non-interactive shells and never source ~/.bashrc.d/, so tools installed by the bootstrap (mise, brew, nix) are not on PATH just because the interactive config would put them there. A script that calls such a tool without first putting it on PATH dies with command not found — even though the tool is installed.
Each run script must therefore re-establish PATH itself, at the top, before invoking the tool. To keep this uniform and in one place, the setup lives in shared .chezmoitemplates partials, included with {{ template "<name>" . }}:
mise-shellenv— prepends~/.local/bin(the mise entry binary) and the mise shims dir. Include it in any script that callsmisedirectly, or a mise-managed tool directly (e.g.pitchfork). Notemise run <task>already exposes that task's managed tools, so formise run …only themisebinary itself needs to be reachable.nix-profile— sources the OS-correct nix profile (macOS multi-user daemon vs Linux single-user), guarded so it is a no-op when nix is absent (safe underset -e).brew-shellenv—evalsbrew shellenvfor the OS/arch-correct prefix.
The interactive ~/.bashrc.d/ files (10-nix, 17-mise, 08-homebrew) use the same partials, so interactive and non-interactive setup share one source of truth. After installing a new bootstrap tool, add a partial for it and include it in both the relevant run script and its ~/.bashrc.d/ file.
Bash Startup Profiling
This feature allows you to measure the startup time of individual files sourced during bash initialization to identify performance bottlenecks.
Usage
Enable profiling by setting the environment variable:
bashexport BASH_PROFILE_TIMING=1Start a new bash session to generate the timing log:
bashbash -lProcess the timing data using the mise task:
bashmise run bash:profile
Output
The task processes the timing data in a pipeline and displays a formatted summary with rankings and percentages. No intermediate files are created.
Example Output
After enabling profiling and running the commands, the output might look like this:
Without activation hooks caching:
$ BASH_PROFILE_TIMING=1 exec bash
Bash profiling enabled. Log will be written to ~/.local/share/dotfiles/bash/profile_timing.log
host : example-host
user : user
shell : bash 5.3.3(1)-release
wd : /home/user
: mise run bash:profile
[bash:profile] $ ~/.config/mise/tasks/bash/profile
Processing bash startup timing log...
=== Bash Startup Timing Summary ===
Rank Sourced File Time (ms) Relative Cumulative
----- ----------------------------------------- --------- -------- -----------
1 /Users/user/.bashrc.d/17-mise.sh 627.363 51.65% 51.65%
2 /Users/user/.bashrc.d/15-devbox.sh 337.218 27.76% 79.41%
3 /Users/user/.bashrc.d/27-chezmoi.sh 66.921 5.51% 84.92%
4 /Users/user/.bashrc.d/82-carapace.sh 42.791 3.52% 88.44%
5 /Users/user/.bashrc.d/19-pixi.sh 42.516 3.50% 91.94%
6 /Users/user/.bashrc.d/40-homebrew.sh 25.505 2.10% 94.04%
7 /Users/user/.bashrc.d/90-direnv.sh 20.107 1.66% 95.70%
8 /Users/user/.bashrc.d/30-atuin.sh 19.015 1.57% 97.26%
9 /Users/user/.bashrc.d/75-zellij.sh 16.325 1.34% 98.60%
10 /Users/user/.bashrc.d/25-zoxide.sh 7.302 0.60% 99.21%
11 /Users/user/.bashrc.d/18-usage.sh 4.817 0.40% 99.60%
12 /Users/user/.bashrc.d/32-nvim.sh 0.328 0.03% 99.63%
13 /Users/user/.bashrc.d/35-functions.sh 0.309 0.03% 99.65%
14 /Users/user/.bashrc.d/05-cache.sh 0.272 0.02% 99.68%
15 /Users/user/.bashrc.d/20-aliases.sh 0.197 0.02% 99.69%
16 /Users/user/.bashrc.d/60-tmux.sh 0.180 0.01% 99.71%
17 /Users/user/.bashrc.d/10-nix.sh 0.170 0.01% 99.72%
18 /Users/user/.bashrc.d/47-amazon-q.sh 0.168 0.01% 99.74%
19 /Users/user/.bashrc.d/45-julia.sh 0.164 0.01% 99.75%
20 /Users/user/.bashrc.d/55-task.sh 0.139 0.01% 99.76%
21 /Users/user/.bashrc.d/48-amazon-q_cargo.sh 0.134 0.01% 99.77%
22 /Users/user/.bashrc.d/50-aichat.sh 0.109 0.01% 99.78%
23 /Users/user/.bashrc.d/57-yazi.sh 0.063 0.01% 99.79%
24 /Users/user/.bashrc.d/52-jupyter.sh 0.044 0.00% 99.79%
TOTAL SOURCED 1212.157 99.79% 99.79%
TOTAL 1214.708 100% 100%With caching (60% faster):
=== Bash Startup Timing Summary ===
Rank Sourced File Time (ms) Relative Cumulative
----- ----------------------------------------- --------- -------- -----------
1 /Users/user/.bashrc.d/17-mise.sh 101.585 21.02% 21.02%
2 /Users/user/.bashrc.d/40-homebrew.sh 58.143 12.03% 33.05%
3 /Users/user/.bashrc.d/15-devbox.sh 50.943 10.54% 43.59%
4 /Users/user/.bashrc.d/30-atuin.sh 47.790 9.89% 53.48%
5 /Users/user/.bashrc.d/90-direnv.sh 39.156 8.10% 61.58%
6 /Users/user/.bashrc.d/25-zoxide.sh 38.471 7.96% 69.54%
7 /Users/user/.bashrc.d/18-usage.sh 37.715 7.80% 77.34%
8 /Users/user/.bashrc.d/75-zellij.sh 37.555 7.77% 85.11%
9 /Users/user/.bashrc.d/82-carapace.sh 35.852 7.42% 92.53%
10 /Users/user/.bashrc.d/19-pixi.sh 18.856 3.90% 96.43%
11 /Users/user/.bashrc.d/27-chezmoi.sh 14.085 2.91% 99.35%
12 /Users/user/.bashrc.d/35-functions.sh 0.098 0.02% 99.37%
13 /Users/user/.bashrc.d/05-cache.sh 0.068 0.01% 99.38%
14 /Users/user/.bashrc.d/55-task.sh 0.067 0.01% 99.40%
15 /Users/user/.bashrc.d/45-julia.sh 0.053 0.01% 99.41%
16 /Users/user/.bashrc.d/20-aliases.sh 0.048 0.01% 99.42%
17 /Users/user/.bashrc.d/10-nix.sh 0.032 0.01% 99.42%
18 /Users/user/.bashrc.d/57-yazi.sh 0.030 0.01% 99.43%
19 /Users/user/.bashrc.d/60-tmux.sh 0.030 0.01% 99.44%
20 /Users/user/.bashrc.d/47-amazon-q.sh 0.028 0.01% 99.44%
21 /Users/user/.bashrc.d/32-nvim.sh 0.025 0.01% 99.45%
22 /Users/user/.bashrc.d/50-aichat.sh 0.021 0.00% 99.45%
23 /Users/user/.bashrc.d/48-amazon-q_cargo.sh 0.020 0.00% 99.46%
24 /Users/user/.bashrc.d/52-jupyter.sh 0.017 0.00% 99.46%
TOTAL SOURCED 480.688 99.46% 99.46%
TOTAL 483.298 100% 100%Notes
- Profiling only works with Bash 5+ (requires
$EPOCHREALTIME) - The timing log is overwritten at the start of each profiling session
- Log is stored at
~/.local/share/dotfiles/bash/profile_timing.log - Only files in
~/.bashrc.d/*.shand/etc/bash_completionare profiled - Disable profiling by unsetting the variable:
unset BASH_PROFILE_TIMING - Caveat: Atuin files bypass profiling to preserve execution context. Atuin's bash-preexec hooks are extremely sensitive to execution context - even function wrappers change the call stack enough to break command recording
Activation Hook Caching
To improve startup performance, mise activation hooks are cached. The cache is automatically refreshed when:
- The mise binary (
~/.local/bin/mise) is updated - Tool installations change (
~/.local/share/mise/installsdirectory timestamp) - Project configuration changes (
mise.tomlin current directory)
The caching implementation includes robust error handling for edge cases:
- First execution: When mise isn't installed yet, activation is skipped gracefully
- Command failures: If
mise activate bashfails, an empty cache is created to prevent errors - Missing output: Commands that produce no output are handled safely
- Binary availability: Mise activation only occurs if the binary is available
Limitations: The cache may not refresh for all configuration changes (e.g., global config modifications, environment-specific configs). If activation hooks seem stale, manually clear the cache:
mise run bash:cleanThis removes all cached activation hooks and forces regeneration on next shell startup.
Bash Configuration Files
The ~/.bashrc.d/ directory contains modular configuration files:
- Shell options and settings
- Tool integrations (mise, pitchfork, etc.)
- Aliases and functions
- Completion configurations
This modular approach makes it easy to manage and profile individual components of the bash startup process.