Skip to content

Instantly share code, notes, and snippets.

@debedb
Last active July 30, 2025 21:35
Show Gist options
  • Save debedb/f8a4b8384cd00b64aa5dfa5f0a961b58 to your computer and use it in GitHub Desktop.
Save debedb/f8a4b8384cd00b64aa5dfa5f0a961b58 to your computer and use it in GitHub Desktop.
Env var sync (OSX)

Environment Variable Sync Tool

This tool automatically syncs environment variables from ~/.bash_profile to macOS LaunchAgent, making them available to GUI applications and non-shell processes.

Problem it solves

When you set environment variables in ~/.bash_profile, they're only available in Terminal sessions. GUI applications (like VS Code, IntelliJ, etc.) launched from Finder or Spotlight don't see these variables.

How it works

  1. Captures current environment variables
  2. Sources ~/.bash_profile to get new environment
  3. Detects changes and new variables
  4. Creates/updates a LaunchAgent plist that sets these variables
  5. Applies changes immediately (full effect after logout/login)

Installation

1. Manual sync (one-time setup)

# Run the sync manually
~/bin/sync-env-to-launchctl

# Check what would change without applying
~/bin/sync-env-to-launchctl --dry-run

# See detailed output
~/bin/sync-env-to-launchctl --verbose

2. Automatic sync (recommended)

Install the LaunchAgent to automatically sync when ~/.bash_profile changes:

# Copy the LaunchAgent to the proper location
cp ~/g/svalka/bin/com.user.sync-env.plist ~/Library/LaunchAgents/

# Load it
launchctl load ~/Library/LaunchAgents/com.user.sync-env.plist

# Verify it's running
launchctl list | grep sync-env

Now whenever you edit ~/.bash_profile, environment variables will sync automatically within 10 seconds.

Usage

After installation:

  1. Edit ~/.bash_profile as normal
  2. Save the file
  3. Within 10 seconds, new variables are available to newly launched apps
  4. For complete effect (all running apps), log out and log back in

Checking logs:

# See sync activity
tail -f /tmp/sync-env.log

# Check for errors
tail -f /tmp/sync-env.error.log

Troubleshooting:

# Check if environment plist exists
ls -la ~/Library/LaunchAgents/com.user.environment.plist

# Check if sync agent is loaded
launchctl list | grep com.user.sync-env

# Manually reload the environment agent
launchctl unload ~/Library/LaunchAgents/com.user.environment.plist
launchctl load ~/Library/LaunchAgents/com.user.environment.plist

# See what environment variables are currently set
launchctl getenv PATH

Features

  • Smart detection: Only syncs variables that changed after sourcing .bash_profile
  • Exclusions: Automatically excludes shell-specific variables (PS1, HISTSIZE, etc.)
  • Backup: Creates timestamped backups before updates
  • Validation: Checks plist syntax before applying
  • Dry-run mode: Preview changes without applying
  • Immediate effect: Variables available to new apps right away

Excluded variables

The tool automatically excludes:

  • Shell internals (BASH_*, SHELL, PWD, etc.)
  • History variables (HIST*)
  • Prompt variables (PS1-PS4)
  • Terminal settings (COLUMNS, LINES, TERM)
  • Functions

Notes

  • Changes apply to newly launched applications immediately
  • Running applications won't see changes until restarted
  • For system-wide effect, log out and log back in
  • The tool creates backups in ~/Library/LaunchAgents/com.user.environment.plist.backup.*

Uninstalling

# Stop automatic sync
launchctl unload ~/Library/LaunchAgents/com.user.sync-env.plist
rm ~/Library/LaunchAgents/com.user.sync-env.plist

# Remove environment variables
launchctl unload ~/Library/LaunchAgents/com.user.environment.plist
rm ~/Library/LaunchAgents/com.user.environment.plist

# Remove the tools
rm ~/g/svalka/bin/sync-env-to-launchctl
rm ~/g/svalka/bin/com.user.sync-env.plist
rm ~/g/svalka/bin/README.md
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.user.sync-env</string>
<key>ProgramArguments</key>
<array>
<string>/Users/alice/bin/sync-env-to-launchctl</string>
</array>
<key>WatchPaths</key>
<array>
<string>/Users/alice/.bash_profile</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>ThrottleInterval</key>
<integer>10</integer>
<key>StandardOutPath</key>
<string>/tmp/sync-env.log</string>
<key>StandardErrorPath</key>
<string>/tmp/sync-env.error.log</string>
</dict>
</plist>
#!/bin/bash
# sync-env-to-launchctl
# Syncs environment variables from ~/.bash_profile to LaunchAgent
set -euo pipefail
# Configuration
PLIST_PATH="$HOME/Library/LaunchAgents/com.user.environment.plist"
BASH_PROFILE="$HOME/.bash_profile"
TEMP_DIR="/tmp/sync-env-$$"
# Variables to exclude from syncing
EXCLUDE_VARS=(
"BASH_ARGC" "BASH_ARGV" "BASH_LINENO" "BASH_SOURCE"
"DIRSTACK" "EUID" "GROUPS" "HOSTNAME" "PPID" "SECONDS"
"SHELLOPTS" "SHLVL" "UID" "_" "COLUMNS" "LINES"
"OLDPWD" "PWD" "SHELL" "TERM" "USER" "USERNAME"
"HISTFILE" "HISTFILESIZE" "HISTSIZE" "HISTCONTROL"
"PROMPT_COMMAND"
"PIPESTATUS" "FUNCNAME" "IFS" "LINENO" "RANDOM"
)
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color
# Helper functions
log() { echo -e "${GREEN}[$(date +'%H:%M:%S')]${NC} $1"; }
warn() { echo -e "${YELLOW}[$(date +'%H:%M:%S')] WARNING:${NC} $1"; }
error() { echo -e "${RED}[$(date +'%H:%M:%S')] ERROR:${NC} $1" >&2; }
cleanup() {
rm -rf "$TEMP_DIR"
}
trap cleanup EXIT
# Check if running in interactive mode (for debugging)
DRY_RUN=false
VERBOSE=false
while [[ $# -gt 0 ]]; do
case $1 in
--dry-run) DRY_RUN=true; shift ;;
--verbose|-v) VERBOSE=true; shift ;;
--help|-h)
echo "Usage: $0 [--dry-run] [--verbose]"
echo " --dry-run Show what would be done without making changes"
echo " --verbose Show detailed output"
exit 0
;;
*) error "Unknown option: $1"; exit 1 ;;
esac
done
# Create temp directory
mkdir -p "$TEMP_DIR"
# Function to check if variable should be excluded
should_exclude() {
local var=$1
for exclude in "${EXCLUDE_VARS[@]}"; do
[[ "$var" == "$exclude" ]] && return 0
done
# Exclude functions and readonly variables
[[ "$var" =~ ^[a-zA-Z_][a-zA-Z0-9_]*\(\)$ ]] && return 0
return 1
}
# Get environment before sourcing
log "Capturing current environment..."
env | sort > "$TEMP_DIR/env_before.txt"
# Source bash profile in a subshell and get new environment
log "Sourcing $BASH_PROFILE..."
(
set +e # Don't exit on errors during sourcing
source "$BASH_PROFILE" 2>/dev/null
env | sort
) > "$TEMP_DIR/env_after.txt"
# Find new or changed environment variables
log "Detecting changes..."
comm -13 "$TEMP_DIR/env_before.txt" "$TEMP_DIR/env_after.txt" > "$TEMP_DIR/env_diff.txt"
# Process the differences
declare -A env_vars
while IFS='=' read -r key value; do
# Skip if no = sign
[[ ! "$key" =~ = ]] || continue
# Skip excluded variables
should_exclude "$key" && continue
# Skip empty keys
[[ -z "$key" ]] && continue
env_vars["$key"]="$value"
[[ "$VERBOSE" == "true" ]] && echo " + $key=$value"
done < "$TEMP_DIR/env_diff.txt"
# Generate plist content
log "Generating LaunchAgent plist..."
cat > "$TEMP_DIR/environment.plist" <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.user.environment</string>
<key>ProgramArguments</key>
<array>
<string>sh</string>
<string>-c</string>
<string>
EOF
# Build the launchctl commands
commands=""
for key in "${!env_vars[@]}"; do
# Escape the value for shell
value="${env_vars[$key]}"
# Escape double quotes and backslashes
value="${value//\\/\\\\}"
value="${value//\"/\\\"}"
commands+=" launchctl setenv \"$key\" \"$value\"\n"
done
# Complete the plist
echo -n "$commands" >> "$TEMP_DIR/environment.plist"
cat >> "$TEMP_DIR/environment.plist" <<'EOF'
</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
EOF
# Validate plist
if ! plutil -lint "$TEMP_DIR/environment.plist" >/dev/null 2>&1; then
error "Generated plist is invalid!"
exit 1
fi
# Show what would be done in dry-run mode
if [[ "$DRY_RUN" == "true" ]]; then
log "DRY RUN: Would update $PLIST_PATH"
echo "--- New environment variables ---"
for key in "${!env_vars[@]}"; do
echo "$key=${env_vars[$key]}"
done
echo "--- Plist content ---"
cat "$TEMP_DIR/environment.plist"
exit 0
fi
# Backup existing plist if it exists
if [[ -f "$PLIST_PATH" ]]; then
backup_path="$PLIST_PATH.backup.$(date +%Y%m%d_%H%M%S)"
cp "$PLIST_PATH" "$backup_path"
log "Backed up existing plist to $backup_path"
fi
# Create LaunchAgents directory if needed
mkdir -p "$(dirname "$PLIST_PATH")"
# Install the new plist
cp "$TEMP_DIR/environment.plist" "$PLIST_PATH"
log "Updated $PLIST_PATH"
# Unload and reload the LaunchAgent
if launchctl list | grep -q "com.user.environment"; then
log "Reloading LaunchAgent..."
launchctl unload "$PLIST_PATH" 2>/dev/null || true
fi
launchctl load "$PLIST_PATH"
# Apply changes immediately to current session
log "Applying environment variables to current session..."
for key in "${!env_vars[@]}"; do
launchctl setenv "$key" "${env_vars[$key]}"
done
log "✅ Environment variables synced successfully!"
log " ${#env_vars[@]} variables updated"
log " Changes will fully take effect for new applications"
log " For complete effect, log out and log back in"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment