Skip to content

Instantly share code, notes, and snippets.

@debedb
Last active July 30, 2025 21:35
Show Gist options
  • Select an option

  • Save debedb/f8a4b8384cd00b64aa5dfa5f0a961b58 to your computer and use it in GitHub Desktop.

Select an option

Save debedb/f8a4b8384cd00b64aa5dfa5f0a961b58 to your computer and use it in GitHub Desktop.

Revisions

  1. debedb revised this gist Jul 30, 2025. 1 changed file with 3 additions and 3 deletions.
    6 changes: 3 additions & 3 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -20,13 +20,13 @@ When you set environment variables in `~/.bash_profile`, they're only available

    ```bash
    # Run the sync manually
    ~/g/svalka/bin/sync-env-to-launchctl
    ~/bin/sync-env-to-launchctl

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

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

    ### 2. Automatic sync (recommended)
  2. debedb created this gist Jul 30, 2025.
    125 changes: 125 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,125 @@
    # 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)

    ```bash
    # Run the sync manually
    ~/g/svalka/bin/sync-env-to-launchctl

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

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

    ### 2. Automatic sync (recommended)

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

    ```bash
    # 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:

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

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

    ### Troubleshooting:

    ```bash
    # 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

    ```bash
    # 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
    ```
    24 changes: 24 additions & 0 deletions com.user.sync-env
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,24 @@
    <?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>
    189 changes: 189 additions & 0 deletions sync-env-to-launchctl
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,189 @@
    #!/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"