• macOS GUI μ•±μ˜ Shell Env 미상속은 macOSκ°€ GUI 앱을 launchd둜 직접 μ‹€ν–‰ν•  λ•Œ μ‚¬μš©μž μ‰˜ μ„€μ •(~/.zshrc)을 λ‘œλ“œν•˜μ§€ μ•Šμ•„ μ‚¬μš©μž μ •μ˜ ν™˜κ²½λ³€μˆ˜κ°€ μ „λ‹¬λ˜μ§€ μ•ŠλŠ” OS 레벨 λ™μž‘
  • 터미널을 ν†΅ν•œ μ‹€ν–‰κ³Ό 달리 Dock/더블클릭 μ‹€ν–‰μ—μ„œλ§Œ λ°œμƒν•˜λŠ” μ‹€ν–‰ 경둜(λΆ€λͺ¨ ν”„λ‘œμ„ΈμŠ€)의 차이
  • Electron, Swift, Objective-C λ“± λͺ¨λ“  macOS GUI 앱이 κ³΅ν†΅μ μœΌλ‘œ κ²ͺλŠ” ν˜„μƒμ΄λ©°, Electron 고유 버그가 μ•„λ‹˜

ν•΄λ‹Ή κ°œλ…μ΄ ν•„μš”ν•œ 이유

  • 개발 ν™˜κ²½(터미널 μ‹€ν–‰)μ—μ„œλŠ” 정상 λ™μž‘ν•˜μ§€λ§Œ prod λΉŒλ“œ(GUI μ‹€ν–‰)μ—μ„œ API ν‚€, μ»€μŠ€ν…€ PATH 등이 사라져 인증 μ‹€νŒ¨λ‚˜ μ»€λ§¨λ“œ not foundκ°€ λ°œμƒ
  • ~/.zshrc에 envλ₯Ό 아무리 잘 섀정해도 prod μ•±μ—λŠ” λ„λ‹¬ν•˜μ§€ μ•ŠλŠ”λ‹€λŠ” 사싀을 λͺ¨λ₯΄λ©΄ 원인 νŒŒμ•…μ— 큰 어렀움

AS-IS (μˆ˜μ • μ „ μ‹€ν–‰ 경둜)

sequenceDiagram
    autonumber
    participant T as 터미널(dev)
    participant L as launchd(prod)
    participant Z as ~/.zshrc
    participant E as Electron App

    Note over T,E: dev μ‹€ν–‰ 경둜
    T->>Z: 둜그인 μ‰˜ 생성 β†’ ~/.zshrc λ‘œλ“œ
    Z-->>T: OPENAI_API_KEY, PATH λ“± env 섀정됨
    T->>E: npm run dev (μžμ‹ ν”„λ‘œμ„ΈμŠ€ β†’ env 상속 βœ…)

    Note over L,E: prod μ‹€ν–‰ 경둜 (μˆ˜μ • μ „)
    L->>E: Dock 더블클릭 β†’ launchdκ°€ 직접 μ‹€ν–‰
    Note over E: ~/.zshrc λ‘œλ“œ μ—†μŒ<br/>env = μ‹œμŠ€ν…œ κΈ°λ³Έκ°’λ§Œ<br/>OPENAI_API_KEY ❌<br/>PATH = /usr/bin:/bin만 ❌

TO-BE (μˆ˜μ • ν›„)

sequenceDiagram
    autonumber
    participant L as launchd
    participant E as Electron (Main)
    participant Z as /bin/zsh
    participant B as Backend Process

    L->>E: μ•± μ‹€ν–‰ (env = μ‹œμŠ€ν…œ κΈ°λ³Έκ°’)
    E->>Z: app.whenReady() β†’ spawn("/bin/zsh", ["-ilc", "env"])
    Z-->>E: stdout: OPENAI_API_KEY=...\nPATH=...50개 env
    E->>E: Object.assign(process.env, parsedEnv)
    E->>B: startBackend({ ...process.env })
    Note over B: μ‰˜ env ν¬ν•¨λœ μƒνƒœλ‘œ μ‹€ν–‰ βœ…

launchd vs 터미널 μ‹€ν–‰μ˜ 차이

macOS ν”„λ‘œμ„ΈμŠ€ κ³„μΈ΅μ—μ„œ λˆ„κ°€ 앱을 μ‹€ν–‰(spawn)ν•˜λŠλƒκ°€ 핡심이닀.

ν•­λͺ©ν„°λ―Έλ„ μ‹€ν–‰ (dev)launchd μ‹€ν–‰ (prod)
λΆ€λͺ¨ ν”„λ‘œμ„ΈμŠ€μ‚¬μš©μž μ‰˜ (zsh/bash)launchd (PID 1)
~/.zshrc λ‘œλ“œβœ…βŒ
μ‚¬μš©μž μ •μ˜ envμ „λΆ€ μƒμ†μ‹œμŠ€ν…œ κΈ°λ³Έκ°’λ§Œ
PATH/opt/homebrew/bin λ“± 포함/usr/bin:/bin:/usr/sbin:/sbin
API ν‚€μžˆμŒμ—†μŒ

μœ λ‹‰μŠ€ env 상속 원칙: μžμ‹ ν”„λ‘œμ„ΈμŠ€λŠ” λΆ€λͺ¨μ˜ envλ₯Ό κ·ΈλŒ€λ‘œ 볡사해 μ‹œμž‘ν•œλ‹€. launchdλŠ” μ‚¬μš©μž μ‰˜μ΄ μ•„λ‹ˆλ―€λ‘œ, μ‚¬μš©μžκ°€ μ‰˜μ— μ •μ˜ν•œ envλ₯Ό μ•Œ 수 μ—†λ‹€.

μ‹€μ œ κ²½ν—˜ν•œ ν˜„μƒ (Electron + Codex CLI μΌ€μ΄μŠ€)

[prod λΉŒλ“œ, μˆ˜μ • μ „]
checkCodexAuth() 호좜
β†’ codex exec probe μ‹€ν–‰
β†’ OPENAI_CODEX_API_KEY μ—†μŒ β†’ not-authenticated ❌

μ—λŸ¬ 둜그 (μ—λŸ¬ 핸듀링 μΆ”κ°€ ν›„ 확인):
Command failed: codex exec --model gpt-5.2-codex say ok
Not inside a trusted directory and --skip-git-repo-check was not specified.

이 μΌ€μ΄μŠ€μ—λŠ” 2개의 λ…λ¦½λœ λ¬Έμ œκ°€ 숨겨져 μžˆμ—ˆλ‹€:

κ΄€λ¬Έλ¬Έμ œμ›μΈ
1μ°¨env 미상속 β†’ API ν‚€ μ—†μŒmacOS launchd 섀계
2μ°¨trusted directory κ±°λΆ€codex CLI λ³΄μ•ˆ μ •μ±… (μ•± λ²ˆλ“€ λ‚΄λΆ€λŠ” git repo μ•„λ‹˜)

1μ°¨μ—μ„œ 이미 μ‹€νŒ¨ν•˜λ―€λ‘œ 2μ°¨ λ¬Έμ œκ°€ prodμ—μ„œ 숨겨져 μžˆμ—ˆκ³ , catch(() => false)κ°€ μ—λŸ¬λ₯Ό μ‚ΌμΌœμ„œ 원인 νŒŒμ•…μ΄ 더 λŠ¦μ–΄μ‘Œλ‹€.

ν•΄κ²° νŒ¨ν„΄: Login Shell Spawn

VS Code(resolveShellEnv()), JetBrains IDE, shell-env npm νŒ¨ν‚€μ§€ λ“± λͺ¨λ“  μ£Όμš” GUI 앱이 λ™μΌν•˜κ²Œ μ‚¬μš©ν•˜λŠ” 업계 ν‘œμ€€ νŒ¨ν„΄μ΄λ‹€.

// electron/services/shell-env.service.ts
async function loadShellEnv(): Promise<void> {
  const shell = os.userInfo().shell ?? '/bin/zsh'
  // -i: interactive, -l: login (~/.zshrc λ‘œλ“œ), -c: command
  const child = spawn(shell, ['-ilc', 'echo DELIM; env; echo DELIM; exit'])
 
  // stdout νŒŒμ‹± β†’ key=value μΆ”μΆœ
  const parsed = parseEnvOutput(stdout)  // ~50개 env λ³€μˆ˜
 
  Object.assign(process.env, parsed)  // process.env에 merge
  // 이후 startBackend({ ...process.env }) β†’ μžλ™ μ „νŒŒ
}
 
// app.whenReady() μ‹œμ μ— 호좜 (λ°±μ—”λ“œ μ‹œμž‘ μ „)
app.whenReady().then(async () => {
  await loadShellEnv()   // 300-400ms μ†Œμš”
  await startBackend()
})

μ‰˜ fallback 체인: κ°μ§€λœ μ‰˜ β†’ /bin/zsh β†’ /bin/bash β†’ process.env κ·ΈλŒ€λ‘œ μ‚¬μš©

Edge case:

  • nushell λ“± λΉ„ν‘œμ€€ μ‰˜ β†’ /bin/zsh둜 fallback
  • ~/.zshrc 문법 였λ₯˜ β†’ /bin/bash둜 fallback
  • 5초 λ‚΄ 응닡 μ—†μŒ β†’ timeout ν›„ μ•± 정상 μ‹œμž‘ (env 없이)

μ°Έκ³  λ¬Έμ„œ