Description
The PAI_DIR environment variable is set to ~/.claude in settings.json, but the tilde (~) is not expanded when accessed as an environment variable in code. This causes hooks and the statusline to fail silently or show incorrect data.
Root Cause
In settings.json:
"env": {
"PAI_DIR": "~/.claude",
...
}
When code accesses process.env.PAI_DIR, it gets the literal string ~/.claude instead of /Users/username/.claude. The shell only expands ~ in certain contexts:
- ✅ At the start of a command:
~/.claude/hooks/foo.ts works
- ❌ From variable expansion:
${PAI_DIR}/hooks/foo.ts → ~/.claude/hooks/foo.ts (tilde NOT expanded)
Impact
- Hook commands fail:
${PAI_DIR}/hooks/StartupGreeting.hook.ts becomes ~/.claude/hooks/... which the shell can't execute
- TypeScript code fails:
process.env.PAI_DIR returns ~/.claude, and fs.readFileSync('~/.claude/settings.json') fails
- Statusline shows "—": Can't read
settings.json to get version number
Steps to Reproduce
# Simulate how Claude Code sets the env var
export PAI_DIR="~/.claude"
# Try to run a hook (fails)
${PAI_DIR}/hooks/StartupGreeting.hook.ts
# Error: no such file or directory: ~/.claude/hooks/StartupGreeting.hook.ts
# Try to read settings in code
node -e "console.log(require('fs').existsSync(process.env.PAI_DIR + '/settings.json'))"
# false
Suggested Fixes
Fix 1: Change hook commands to use literal ~ (portable)
In settings.json, change from:
"command": "${PAI_DIR}/hooks/SecurityValidator.hook.ts"
To:
"command": "~/.claude/hooks/SecurityValidator.hook.ts"
The tilde works when it's at the start of the command string.
Fix 2: Add tilde expansion in shell scripts
In statusline-command.sh:
PAI_DIR="${PAI_DIR:-$HOME/.claude}"
# Add this line to expand tilde:
PAI_DIR="${PAI_DIR/#\~/$HOME}"
Fix 3: Use centralized path resolution in TypeScript
The lib/paths.ts already has getPaiDir() with proper expansion. All hooks should use it instead of direct process.env.PAI_DIR access:
// BAD - tilde not expanded
const baseDir = process.env.PAI_DIR || join(process.env.HOME!, '.claude');
// GOOD - uses centralized expansion
import { getPaiDir } from './lib/paths';
const baseDir = getPaiDir();
Affected Files
settings.json
- All hook command paths using
${PAI_DIR}
- statusLine command using
${PAI_DIR}
TypeScript files using direct PAI_DIR access
hooks/lib/IdealState.ts
hooks/lib/notifications.ts
hooks/AutoWorkCreation.hook.ts
hooks/ImplicitSentimentCapture.hook.ts
hooks/ExplicitRatingCapture.hook.ts
Shell scripts
Environment
- PAI Version: 2.3.0
- Claude Code Version: 2.1.17
- OS: macOS (also affects Linux)
- Shell: zsh/bash
Related
This is a different issue from #477 (stderr causing false errors). Both can appear as "hook error" but have different root causes.
Description
The
PAI_DIRenvironment variable is set to~/.claudein settings.json, but the tilde (~) is not expanded when accessed as an environment variable in code. This causes hooks and the statusline to fail silently or show incorrect data.Root Cause
In
settings.json:When code accesses
process.env.PAI_DIR, it gets the literal string~/.claudeinstead of/Users/username/.claude. The shell only expands~in certain contexts:~/.claude/hooks/foo.tsworks${PAI_DIR}/hooks/foo.ts→~/.claude/hooks/foo.ts(tilde NOT expanded)Impact
${PAI_DIR}/hooks/StartupGreeting.hook.tsbecomes~/.claude/hooks/...which the shell can't executeprocess.env.PAI_DIRreturns~/.claude, andfs.readFileSync('~/.claude/settings.json')failssettings.jsonto get version numberSteps to Reproduce
Suggested Fixes
Fix 1: Change hook commands to use literal
~(portable)In
settings.json, change from:To:
The tilde works when it's at the start of the command string.
Fix 2: Add tilde expansion in shell scripts
In
statusline-command.sh:Fix 3: Use centralized path resolution in TypeScript
The
lib/paths.tsalready hasgetPaiDir()with proper expansion. All hooks should use it instead of directprocess.env.PAI_DIRaccess:Affected Files
settings.json
${PAI_DIR}${PAI_DIR}TypeScript files using direct PAI_DIR access
hooks/lib/IdealState.tshooks/lib/notifications.tshooks/AutoWorkCreation.hook.tshooks/ImplicitSentimentCapture.hook.tshooks/ExplicitRatingCapture.hook.tsShell scripts
statusline-command.shEnvironment
Related
This is a different issue from #477 (stderr causing false errors). Both can appear as "hook error" but have different root causes.