Skip to content

Exposes environment variables to GUI applications on macOS#206

Open
crdant wants to merge 5 commits intomainfrom
feature/crdant/adds-gui-environment-variables
Open

Exposes environment variables to GUI applications on macOS#206
crdant wants to merge 5 commits intomainfrom
feature/crdant/adds-gui-environment-variables

Conversation

@crdant
Copy link
Owner

@crdant crdant commented Oct 20, 2025

Exposes environment variables to GUI applications on macOS

TL;DR

Configures per-module launchd agents that propagate shell environment variables to GUI applications at login, enabling tools like Claude Desktop, VS Code, and Kubernetes clients to access necessary configuration paths, binaries, and SSH authentication.

Details

GUI applications launched from Finder, Spotlight, or the Dock on macOS operate in a different environment context than terminal sessions. They don't inherit the carefully configured PATH entries, EDITOR settings, XDG directories, SSH agent sockets, or tool-specific configuration paths that developers rely on in their shells. This disconnect creates friction when GUI applications attempt to spawn editors, execute binaries from custom paths, authenticate via SSH, or locate configuration files.

The implementation addresses this by creating lightweight launchd agents that run at user login and use launchctl setenv to set environment variables in the macOS launch services environment. Rather than a monolithic approach, each module that needs GUI environment access manages its own agent, keeping configuration co-located and allowing modules to be independently enabled or disabled.

The base module establishes foundational environment for all GUI applications by setting PATH to include ~/.local/bin, ~/workspace/go/bin, and Homebrew paths, along with EDITOR, VISUAL, XDG_CONFIG_HOME, and SSH_AUTH_SOCK pointing to the GPG agent socket. The SSH_AUTH_SOCK configuration enables GUI applications to use Yubikey-backed GPG agent authentication, matching the shell behavior. The AI module sets CLAUDE_CONFIG_DIR with conditional logic that matches the shell configuration (replicated vs personal contexts based on username). The kubernetes module intelligently extends PATH with the krew bin directory, checking for existing PATH values to avoid duplicates and maintain proper ordering.

An activation script ensures the log directory exists at ~/.local/state/launchd/ before agents attempt to write their output, providing visibility for troubleshooting without cluttering system logs. The agents execute only once at login via RunAtLoad, consuming no ongoing system resources.

This enables Git GUI clients to use nvim as configured and authenticate with SSH keys via GPG agent, allows Claude Desktop to find its configuration and custom commands, ensures VS Code terminals inherit the full development PATH and SSH authentication, and lets Kubernetes GUI tools locate krew plugins. The shell environment remains completely unchanged, making this purely additive functionality with graceful degradation for users who don't logout after the update.

Implementation follows the existing pattern established in the certificates module and uses Home Manager's launchd.agents configuration exclusively on Darwin systems via conditional blocks. All three modules (base, ai, kubernetes) build successfully and create properly formatted launchd property lists.

Relevant Research: docs/research/2025-10-18-darwin-desktop-app-environment.md

Implementation Plan: docs/plans/2025-10-20-gui-environment-variables.md

Summary by CodeRabbit

  • Documentation

    • Added research doc on making terminal environment variables available to macOS GUI/Desktop apps.
    • Added a comprehensive rollout and verification plan for macOS GUI environment variable support.
  • New Features

    • macOS-only launch agents added (base, AI, Kubernetes) to expose PATH, EDITOR, VISUAL, XDG_CONFIG_HOME, CLAUDE_CONFIG_DIR, and related logs to GUI sessions.

crdant and others added 4 commits October 20, 2025 11:19
Creates a detailed plan to make terminal environment variables available
to GUI/Desktop applications on macOS using Home Manager launchd agents.

The plan implements per-module launchd agents for:
- base module: PATH, EDITOR, VISUAL, XDG_CONFIG_HOME
- ai module: CLAUDE_CONFIG_DIR
- kubernetes module: krew PATH addition

Each agent uses launchctl setenv to propagate variables to the GUI
session at user login. This enables GUI apps like Claude Desktop, VS Code,
and Kubernetes tools to access environment that was previously shell-only.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…ns on macOS

GUI applications on macOS don't inherit shell environment variables, causing
issues with tools like Claude Desktop, VS Code, and Kubernetes clients that
need access to PATH, config directories, and other environment settings.

This implements per-module launchd agents that run at login to set environment
variables in the macOS launch services environment:

- Base module: Sets PATH, EDITOR, VISUAL, and XDG_CONFIG_HOME for all GUI apps
- AI module: Sets CLAUDE_CONFIG_DIR based on username (personal vs replicated)
- Kubernetes module: Adds krew bin directory to PATH for kubectl plugins

Also adds activation script to create log directory for launchd agent output.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Configures the base module's launchd agent to expose SSH_AUTH_SOCK pointing
to the GPG agent SSH socket (~/.gnupg/S.gpg-agent.ssh). This enables GUI
applications like Git clients, VS Code, and other macOS applications to use
the Yubikey-backed GPG agent for SSH authentication, matching the behavior
already configured for shell sessions in zsh initExtra.

Without this configuration, GUI applications would not have access to the
GPG agent socket and would fall back to default SSH authentication methods,
bypassing the Yubikey security.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Oct 20, 2025

Walkthrough

Adds Darwin-only per-module launchd user agents (base, ai, kubernetes) that expose selected shell environment variables to macOS GUI sessions, plus research and rollout documentation describing design, verification, logging, and migration steps.

Changes

Cohort / File(s) Summary
Documentation: Research & Plan
docs/research/2025-10-18-darwin-desktop-app-environment.md, docs/plans/2025-10-20-gui-environment-variables.md
New research and implementation plan describing approaches, phased rollout, verification steps, integration points, and migration considerations for propagating shell env vars to Darwin GUI apps via launchd agents.
Base module: launchd agent
home/modules/base/default.nix
Adds Darwin-only io.crdant.env.base launchd agent configured to set PATH, EDITOR, VISUAL, XDG_CONFIG_HOME, SSH_AUTH_SOCK; adds creation of ${config.xdg.stateHome}/launchd for agent logs.
AI module: launchd agent
home/modules/ai/default.nix
Adds Darwin-only io.crdant.env.ai launchd agent that sets CLAUDE_CONFIG_DIR with user-specific logic and stdout/stderr log paths.
Kubernetes module: launchd agent
home/modules/kubernetes/default.nix
Adds Darwin-only io.crdant.env.kubernetes launchd agent that prepends ${homeDirectory}/.krew/bin to PATH at RunAtLoad and writes logs to launchd state paths.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant macOS
    participant launchd
    participant Agent_base as io.crdant.env.base
    participant Agent_ai as io.crdant.env.ai
    participant Agent_kube as io.crdant.env.kubernetes
    participant GUIApp

    User->>macOS: Start GUI session / login
    macOS->>launchd: Load user agents
    launchd->>Agent_base: RunAtLoad (setenv PATH, EDITOR, VISUAL, XDG_CONFIG_HOME, SSH_AUTH_SOCK)
    launchd->>Agent_ai: RunAtLoad (setenv CLAUDE_CONFIG_DIR)
    launchd->>Agent_kube: RunAtLoad (setenv PATH with .krew/bin)
    Note right of launchd: Agents call launchctl setenv to register variables
    User->>GUIApp: Launch GUI application
    launchd->>GUIApp: GUI process inherits registered environment
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped to the macOS glade today,

launchd whispers envs in bright array,
PATH and CLAUDE find their place,
GUI apps wake with a friendly face,
Logs tucked safe — a tidy rabbit's day.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "Exposes environment variables to GUI applications on macOS" directly and clearly summarizes the main objective of this pull request. The changeset adds per-module launchd agents configured via Home Manager that propagate shell environment variables (PATH, EDITOR, VISUAL, XDG_CONFIG_HOME, SSH_AUTH_SOCK, CLAUDE_CONFIG_DIR) to macOS GUI applications at user login through launchctl setenv, which is exactly what the title conveys. The title is specific and descriptive, avoiding vague terms, and accurately represents the core purpose of all modifications across the base, ai, and kubernetes modules along with the supporting documentation files.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/crdant/adds-gui-environment-variables

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
home/modules/base/default.nix (1)

373-374: Consider computing PATH dynamically to avoid maintenance drift.

The hardcoded PATH duplicates the logic from sessionPath (lines 21-30) and could drift out of sync over time. If Homebrew paths or custom directories are added to sessionPath, this launchd PATH must be manually updated.

Consider computing the PATH dynamically or extracting it to a shared variable:

let
  # Define base paths that both shell and GUI need
  basePaths = [
    "${config.home.homeDirectory}/.local/bin"
    "${config.home.homeDirectory}/workspace/go/bin"
  ] ++ lib.optionals isDarwin [
    "/opt/homebrew/bin"
    "/opt/homebrew/sbin"
  ] ++ [
    "/usr/local/bin"
    "/usr/local/sbin"
    "/usr/bin"
    "/bin"
    "/usr/sbin"
    "/sbin"
  ];
  pathString = lib.concatStringsSep ":" basePaths;
in

Then reference pathString in the launchd agent and derive sessionPath from basePaths.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f5fe9a6 and 54d4ebf.

📒 Files selected for processing (5)
  • docs/plans/2025-10-20-gui-environment-variables.md (1 hunks)
  • docs/research/2025-10-18-darwin-desktop-app-environment.md (1 hunks)
  • home/modules/ai/default.nix (1 hunks)
  • home/modules/base/default.nix (2 hunks)
  • home/modules/kubernetes/default.nix (1 hunks)
🧰 Additional context used
🪛 LanguageTool
docs/plans/2025-10-20-gui-environment-variables.md

[grammar] ~75-~75: Ensure spelling is correct
Context: ...tiate --eval` #### Manual Verification: 1. Logout and log back in (or reboot) 2. **Veri...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🪛 markdownlint-cli2 (0.18.1)
docs/research/2025-10-18-darwin-desktop-app-environment.md

135-135: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


161-161: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


175-175: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

docs/plans/2025-10-20-gui-environment-variables.md

18-18: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


50-50: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


427-427: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


433-433: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


438-438: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


443-443: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


510-510: Bare URL used

(MD034, no-bare-urls)


511-511: Bare URL used

(MD034, no-bare-urls)

🔇 Additional comments (5)
home/modules/ai/default.nix (1)

79-104: LGTM! Clean implementation of GUI environment propagation.

The launchd agent correctly replicates the shell logic from lines 67-74, ensuring GUI apps receive the same CLAUDE_CONFIG_DIR configuration. The use of config.xdg.configHome and proper logging paths follows the established pattern.

docs/plans/2025-10-20-gui-environment-variables.md (1)

1-511: Excellent documentation - comprehensive and implementation-aligned.

This plan document thoroughly details the phased approach, verification steps, and success criteria. The content accurately reflects the actual implementation across all three modules.

docs/research/2025-10-18-darwin-desktop-app-environment.md (1)

1-327: Thorough research that informed the implementation well.

This research document provides valuable context and explores multiple approaches before settling on the per-module launchd agent pattern. The analysis correctly identified the gaps and recommended a solution that was successfully implemented.

home/modules/base/default.nix (2)

77-79: LGTM! Log directory creation is properly ordered.

Creating the launchd log directory before writeBoundary ensures the directory exists when the agents attempt to write their output.


383-384: Address inconsistency between shell and launchctl SSH_AUTH_SOCK configuration.

The standard macOS GPG agent SSH socket location is ~/.gnupg/S.gpg-agent.ssh, but GnuPG documentation recommends using gpgconf --list-dirs agent-ssh-socket to determine it programmatically rather than hardcoding. The shell configuration (lines 230-244) correctly uses gpgconf to dynamically detect the socket path, but the launchctl environment setup (line 384) hardcodes it. This inconsistency means that users with custom GPG configurations will have the correct path in shell but potentially incorrect path in GUI applications via launchctl.

While the hardcoded path works for standard installs, consider either:

  • Using the same dynamic detection approach in launchctl (with an absolute path to gpgconf)
  • Storing the path in a Nix variable referenced in both locations
  • Documenting why the hardcoded value is acceptable for the launchctl context

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 54d4ebf and d84b37f.

📒 Files selected for processing (1)
  • home/modules/kubernetes/default.nix (1 hunks)
🔇 Additional comments (4)
home/modules/kubernetes/default.nix (4)

47-48: LGTM! Conditional launchd enable follows Home Manager patterns.

The Darwin-only conditional correctly gates the launchd configuration.


50-53: LGTM! Agent configuration structure follows launchd conventions.

The agent naming (reverse domain notation) and label configuration are correct.


65-65: Previous feedback addressed: delimiter-aware PATH check implemented correctly.

The colon-wrapping pattern now correctly prevents false positives from partial path matches like /.krew/bin-old.


71-73: LGTM! RunAtLoad and logging configuration are appropriate.

Running at login (RunAtLoad) aligns with the goal of exposing environment variables to GUI applications. The log paths correctly use xdg.stateHome, and the PR objectives confirm an activation script ensures the directory exists.

Comment on lines +54 to +70
ProgramArguments = [
"${pkgs.bash}/bin/bash"
"-c"
''
# Add krew bin to PATH for GUI Kubernetes tools
CURRENT_PATH=$(launchctl getenv PATH)
if [[ -z "$CURRENT_PATH" ]]; then
# If PATH doesn't exist yet, set a basic one
launchctl setenv PATH "${config.home.homeDirectory}/.krew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
else
# Prepend krew to existing PATH if not already present
if [[ ":$CURRENT_PATH:" != *":${config.home.homeDirectory}/.krew/bin:"* ]]; then
launchctl setenv PATH "${config.home.homeDirectory}/.krew/bin:$CURRENT_PATH"
fi
fi
''
];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Potential race condition with base module's PATH agent.

The check-then-act pattern (read PATH at line 59, conditionally write at lines 62, 66) is susceptible to TOCTOU races if the base module's launchd agent runs concurrently. Since launchd provides no ordering guarantees between agents, both might read the same initial PATH, then sequentially write it, causing one to overwrite the other's changes. The result could be missing PATH entries (either krew bin or base module's entries).

Run the following script to examine how the base module configures PATH and assess the interaction:


🏁 Script executed:

#!/bin/bash
# Check base module's launchd agent PATH configuration
ast-grep --pattern 'launchd = $_ {
  $$$
  agents = {
    $$$
    launchctl setenv PATH $$$
    $$$
  }
  $$$
}'

# Also search for base module PATH setup with context
rg -nP -A10 -B5 --type=nix 'io\.crdant\.env\.base|launchctl setenv PATH' home/modules/base/

Length of output: 1883


I need to gather more information about the launchd agent startup behavior and whether both agents are actually active. Let me search the kubernetes module and verify the claims about launchd concurrency.<!-- [scratchpad_update]
[task verify_race_condition_claim]
Searches completed; critical information gathered on launchd concurrency behavior.

[observations]

  • Web search confirms: "There's no specific order to the LaunchDaemons folders -- everything in them becomes eligible to run all at once" and "there's no particular order, they all become eligible to run at the same time"
  • The launchd daemon was designed to remove the need for dependency ordering among daemons
  • Both modules use launchctl setenv PATH which is a full replacement (not append)
  • Review concern is technically valid: concurrent execution of two agents that both call launchctl setenv PATH could indeed cause one to overwrite the other

[pending]

  • Verify: Is the kubernetes module actually ENABLED by default in this config?
  • Verify: Does the kubernetes launchd agent have RunAtLoad true?
  • Check: File structure to confirm both agents would be loaded

[actions]

  • Search kubernetes module to confirm it's enabled and agent configuration
  • [scratchpad_end] -->

Now let me check if the kubernetes module is actually enabled and review the agent configuration more carefully:<!-- [scratchpad_update_2]
[task verify_race_condition_claim]
Web search completed on launchctl setenv PATH behavior. Key findings:

[observations]
From search results:

  • No direct documentation on race conditions between concurrent launchctl setenv PATH calls
  • Results show PATH is special/problematic (multiple sources cite PATH issues with launchctl)
  • The check-then-act pattern in kubernetes code uses launchctl getenv PATH and conditionally calls launchctl setenv PATH
  • Both agents run at login time (not sequentially), all LaunchAgents "become eligible to run all at once"
  • Sequential execution of launchctl setenv PATH appears to result in last-write-wins (overwrite)

[pending]

  • Need to examine kubernetes module to confirm agent is actually enabled
  • Need to see if both agents have RunAtLoad true (which would make both execute at login)
  • Need to verify if the kubernetes module actually gets built into the user's config

[actions]

  • Search the kubernetes/default.nix file for the launchd agent setup
  • Check if kubernetes module is conditionally enabled
  • [scratchpad_end] -->

Let me check the kubernetes module configuration and verify if the launchd agent is actually enabled:


🏁 Script executed:

#!/bin/bash
# Find and examine the kubernetes module's launchd agent configuration
fd --type f "kubernetes" home/modules/ | head -5

# Check the kubernetes module for launchd agent setup with context
rg -nP -A30 -B5 --type=nix 'launchd|krew' home/modules/kubernetes/

Length of output: 165


The paths don't exist in the current repository state. Let me search more broadly to understand the repository structure and verify if the kubernetes module is actually present:


🏁 Script executed:

#!/bin/bash
# Check if home/modules directory exists and list its contents
ls -la home/modules/ 2>/dev/null || echo "home/modules/ does not exist"

# Search entire repository for any 'krew' references
rg -nP --type=nix 'krew' . 2>/dev/null || echo "No krew references found"

# Search for kubernetes module references
rg -nP --type=nix 'kubernetes|io\.crdant\.env\.kubernetes' . 2>/dev/null || echo "No kubernetes module references found"

# List top-level directory structure
ls -la . | head -20

Length of output: 3824


Now let me examine the full kubernetes module configuration and check for RunAtLoad settings:


🏁 Script executed:

#!/bin/bash
# Get the full kubernetes module to see RunAtLoad and complete agent config
cat ./home/modules/kubernetes/default.nix

# Check for any ordering/dependency directives between agents
rg -nP --type=nix 'after|before|requires|wants|RunAtLoad' ./home/modules/kubernetes/ ./home/modules/base/

Length of output: 2056


Now let me check the module loading order in the profiles to understand if there's a defined execution sequence:


🏁 Script executed:

#!/bin/bash
# Check the profiles to see module loading order
cat ./home/profiles/full.nix
echo "---"
cat ./home/profiles/development.nix
echo "---"
# Also check base profile
cat ./home/profiles/base.nix 2>/dev/null || echo "No base.nix found"

Length of output: 1172


Let me search for any launchd ordering mechanisms in home-manager or the config:


🏁 Script executed:

#!/bin/bash
# Check for any launchd ordering/dependency support in the configs
rg -nP --type=nix 'StandardIn|StandardOut|wait|depends|order' ./home/modules/kubernetes/ ./home/modules/base/

# Search home-manager docs reference or ordering patterns
rg -nP --type=nix 'launchd.*before|launchd.*after' .

# Check if there's any conditional enable logic
rg -nP -B3 -A3 'enable.*true|enable.*false' ./home/modules/kubernetes/default.nix | head -30

Length of output: 492


Concurrent write to launchd PATH may cause krew bin entry to be lost.

The concern is confirmed. Both agents execute concurrently at login with RunAtLoad = true and no ordering guarantees. The base module sets an absolute PATH without krew bin (home/modules/base/default.nix:374), while kubernetes module attempts to prepend it (home/modules/kubernetes/default.nix:58-66). If base's agent runs after kubernetes's, it overwrites the PATH with its absolute value, removing the krew bin entry.

Fix: Either include ${config.home.homeDirectory}/.krew/bin in the base module's absolute PATH, or ensure kubernetes agent runs after base (if home-manager's launchd supports ordering).

🤖 Prompt for AI Agents
In home/modules/kubernetes/default.nix around lines 54 to 70, the launchd agent
prepends the krew bin to PATH but can be overwritten because another agent in
home/modules/base/default.nix (around line ~374) sets an absolute PATH without
krew; fix by either adding ${config.home.homeDirectory}/.krew/bin to the
absolute PATH in the base module so the krew entry is always present, or adjust
launchd ordering so the kubernetes agent runs after the base agent (if
supported) to ensure the prepend is not clobbered.

Comment on lines +60 to +62
if [[ -z "$CURRENT_PATH" ]]; then
# If PATH doesn't exist yet, set a basic one
launchctl setenv PATH "${config.home.homeDirectory}/.krew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Hardcoded fallback PATH may diverge from base module configuration.

The fallback PATH on line 62 is hardcoded. If the base module's PATH configuration changes (e.g., adding Homebrew paths, Go bin, etc. as mentioned in the PR objectives), this fallback won't reflect those changes, creating maintenance burden and potential inconsistencies.

Consider either removing this fallback (assuming the base agent always runs) or sourcing the PATH components from a shared configuration.

🤖 Prompt for AI Agents
In home/modules/kubernetes/default.nix around lines 60–62, the launchctl
fallback PATH is hardcoded which can drift from the base module's PATH
configuration; replace this hardcoded string by either removing the fallback
entirely (assume the base agent always provides PATH) or deriving the value from
the shared/base configuration: reference the base module's PATH/path list (e.g.
the exported environment.path or the shared paths variable), join it into a
colon-separated string, and use that expression for launchctl setenv so future
changes to the base module are reflected automatically.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant