Exposes environment variables to GUI applications on macOS#206
Exposes environment variables to GUI applications on macOS#206
Conversation
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>
WalkthroughAdds 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
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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 tosessionPath, 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; inThen reference
pathStringin the launchd agent and derivesessionPathfrombasePaths.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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.configHomeand 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
writeBoundaryensures 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-socketto determine it programmatically rather than hardcoding. The shell configuration (lines 230-244) correctly usesgpgconfto 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>
There was a problem hiding this comment.
Actionable comments posted: 2
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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.
| 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 | ||
| '' | ||
| ]; |
There was a problem hiding this comment.
🧩 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 PATHwhich is a full replacement (not append) - Review concern is technically valid: concurrent execution of two agents that both call
launchctl setenv PATHcould 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 PATHcalls - Results show PATH is special/problematic (multiple sources cite PATH issues with launchctl)
- The check-then-act pattern in kubernetes code uses
launchctl getenv PATHand conditionally callslaunchctl 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 -20Length 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 -30Length 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.
| 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" |
There was a problem hiding this comment.
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.
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 setenvto 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
New Features