Skip to content

Tilde (~) in PAI_DIR env variable not expanded, causing hook and statusline failures #478

Description

@flexorflex

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

  1. Hook commands fail: ${PAI_DIR}/hooks/StartupGreeting.hook.ts becomes ~/.claude/hooks/... which the shell can't execute
  2. TypeScript code fails: process.env.PAI_DIR returns ~/.claude, and fs.readFileSync('~/.claude/settings.json') fails
  3. 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

  • statusline-command.sh

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions