# How to Manage Environment Variables in Dotfiles Securely: A Defence-in-Depth Guide

> Securely manage environment variables in dotfiles by never committing secrets. Store safe defaults in tracked files and source personal secrets from untracked local files for defense-in-depth.

- Repository: [Ivan Smirnov/dotfiles](https://github.com/issmirnov/dotfiles)
- Tags: best-practices
- Published: 2026-03-04

---

**The most secure way to manage environment variables in dotfiles is to never commit secrets to version control, store only safe defaults in tracked files like [`zsh/config/env.zsh`](https://github.com/issmirnov/dotfiles/blob/main/zsh/config/env.zsh), and source personal secrets from an untracked local file such as `~/.zshrc.local`.**

Managing sensitive credentials alongside public configuration requires a careful balance between convenience and security. The `issmirnov/dotfiles` repository demonstrates a robust, **defence-in-depth** approach to handle environment variables that keeps secrets out of Git while maintaining a reproducible baseline across machines. This guide examines the specific patterns implemented in the source code to help you manage environment variables in dotfiles securely.

## The Three-Layer Security Model

The repository implements a layered strategy that separates public configuration from private data, ensuring that sensitive values never appear in commit history.

### Layer 1: Exclude Secrets from Version Control

The first line of defence is the top-level `.gitignore` file, which explicitly excludes any file matching `*.env` and similar dotenv patterns. This rule is reinforced in auxiliary locations: `ubersicht/.gitignore` and `git/template/info/exclude` both contain identical exclusions with explanatory comments. These multiple exclusion points guarantee that temporary or local environment files remain untracked even when generated by third-party tools.

### Layer 2: Version-Control Safe Defaults

Non-sensitive variables that should be shared across all machines live in [`zsh/config/env.zsh`](https://github.com/issmirnov/dotfiles/blob/main/zsh/config/env.zsh). This file exports reproducible baseline settings such as `LC_ALL`, `LANG`, `TERM`, `EDITOR`, `VISUAL`, and `XDG_CONFIG_HOME`. Because these values contain no secrets, they can be safely committed and cloned to any workstation without risk of credential leakage.

### Layer 3: Local Overrides for Private Data

The main Zsh entry point at `zsh/zshrc` implements conditional sourcing for machine-specific secrets. At line 66, the configuration checks for the existence of a local override file before sourcing it:

```zsh
[[ -f ~/.zshrc.local ]] && source ~/.zshrc.local

```

Because `~/.zshrc.local` resides outside the repository structure, it never appears in `git status` or diff outputs, making it the canonical location for API tokens, cloud credentials, and proprietary configuration.

## Implementation Details from the Source Code

The `issmirnov/dotfiles` repository separates concerns across specific files to maintain this security boundary.

**Safe, tracked variables** in [`zsh/config/env.zsh`](https://github.com/issmirnov/dotfiles/blob/main/zsh/config/env.zsh):

```zsh

# Locale & terminal

export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8
export TERM=xterm-256color

# Preferred editors

export EDITOR=vim
export VISUAL=vim

# XDG base directory

export XDG_CONFIG_HOME="$HOME/.config"

```

**Untracked secrets** in `~/.zshrc.local` (create this manually):

```zsh

# Example: AWS credentials (never commit!)

export AWS_ACCESS_KEY_ID="AKIA…"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"

# Example: GitHub token for CLI tools

export GH_TOKEN="ghp_XXXXXXXXXXXXXXXXXXXXXXXX"

```

The conditional check in `zsh/zshrc` ensures that shell initialization continues normally even if the local file is absent, preventing errors on fresh installs or CI environments.

## Step-by-Step Workflow for Adding Secrets

Follow this exact pattern to manage environment variables in dotfiles securely when adding new credentials:

1. **Create or edit your local override file**:

```bash
nano ~/.zshrc.local

```

2. **Add your sensitive exports** using standard shell syntax:

```zsh
export MY_API_KEY="secret_value_here"

```

3. **Verify the file remains untracked**:

```bash
git status

```

The output should not list `.zshrc.local` or any `.env` files, confirming that `.gitignore` rules are active.

## Auditing Your Configuration

To verify that your exclusion rules are working correctly before attempting to commit sensitive data, use Git's built-in ignore checker:

```bash
git check-ignore -v .env

```

A properly configured repository will return the matching pattern and file location, such as:

```

.gitignore:3:*.env    .env

```

This confirmation step provides assurance that files matching the pattern are definitively excluded from the index.

## Summary

- **Never commit raw secrets**: The `.gitignore` file at the repository root excludes `*.env` patterns, while `git/template/info/exclude` and `ubersicht/.gitignore` provide redundant protection.
- **Store safe defaults in tracked files**: Use [`zsh/config/env.zsh`](https://github.com/issmirnov/dotfiles/blob/main/zsh/config/env.zsh) for locale, terminal, and editor variables that can be shared across all machines.
- **Source private data locally**: The `zsh/zshrc` file conditionally sources `~/.zshrc.local`, creating a secure boundary for machine-specific credentials.
- **Verify exclusions**: Use `git check-ignore` to audit patterns before adding sensitive files.

## Frequently Asked Questions

### What happens if I accidentally commit a .env file to my dotfiles repository?

If a secret is committed, immediately rotate the compromised credentials and use `git filter-repo` or `BFG Repo-Cleaner` to remove the file from history. After cleanup, add the file name to `.gitignore` and verify with `git check-ignore` to prevent recurrence. The `issmirnov/dotfiles` repository prevents this scenario by excluding `.env` patterns before any commits are made.

### Can I use this pattern with Bash instead of Zsh?

Yes. The conditional sourcing pattern `[[ -f ~/.bashrc.local ]] && source ~/.bashrc.local` works identically in Bash. Place this check at the end of your `~/.bashrc` file, and store your secrets in `~/.bashrc.local`, which should be listed in `.gitignore`. The security model remains the same across POSIX-compliant shells.

### How do I migrate existing secrets out of my dotfiles repository?

First, move all sensitive `export` statements from your tracked configuration files into a new `~/.zshrc.local` file. Then run `git rm --cached` on any files that previously contained secrets to remove them from the index while preserving local copies. Finally, add those file patterns to `.gitignore` and commit the changes. Verify with `git log --full-history -- <filename>` to ensure secrets no longer appear in the history.

### Should I encrypt my ~/.zshrc.local file for additional security?

While the local file approach keeps secrets out of version control, encrypting `~/.zshrc.local` adds protection against physical machine compromise or backup theft. Tools like `git-crypt`, `sops`, or `age` can encrypt the file at rest, requiring decryption at shell startup. However, ensure the decryption key is not stored in the dotfiles repository itself, or you reintroduce the original security risk.