@@ -464,7 +464,7 @@ def configure_all_mcp_servers(servers: list[dict[str, Any]]) -> bool:
464464 return True
465465
466466
467- def create_additional_settings (hooks : list [ dict [str , Any ] ], claude_user_dir : Path ) -> bool :
467+ def create_additional_settings (hooks : dict [str , Any ], claude_user_dir : Path ) -> bool :
468468 """Create additional-settings.json with environment-specific hooks.
469469
470470 This file is always overwritten to avoid duplicate hooks when re-running the installer.
@@ -492,33 +492,32 @@ def create_additional_settings(hooks: list[dict[str, Any]], claude_user_dir: Pat
492492 'hooks' : {},
493493 }
494494
495- # Process each hook
496- for hook in hooks :
497- # Ensure hook is a dict (should be with PyYAML)
498- if not isinstance (hook , dict ):
499- warning (f'Unexpected hook format: { type (hook )} - { hook } ' )
500- continue
495+ # Extract files and events from the hooks configuration
496+ hook_files = hooks .get ('files' , [])
497+ hook_events = hooks .get ('events' , [])
498+
499+ # Download all hook files first
500+ if hook_files :
501+ hooks_dir = claude_user_dir / 'hooks'
502+ hooks_dir .mkdir (parents = True , exist_ok = True )
503+ for file in hook_files :
504+ url = f'{ REPO_BASE_URL } /{ file } '
505+ filename = Path (file ).name
506+ destination = hooks_dir / filename
507+ download_file (url , destination )
508+
509+ # Process each hook event
510+ for hook in hook_events :
501511
502512 event = hook .get ('event' )
503513 matcher = hook .get ('matcher' , '' )
504514 hook_type = hook .get ('type' , 'command' )
505515 command = hook .get ('command' )
506- files = hook .get ('files' , [])
507516
508517 if not event or not command :
509518 warning ('Invalid hook configuration, skipping' )
510519 continue
511520
512- # Download hook files if specified
513- if files :
514- hooks_dir = claude_user_dir / 'hooks'
515- hooks_dir .mkdir (parents = True , exist_ok = True )
516- for file in files :
517- url = f'{ REPO_BASE_URL } /{ file } '
518- filename = Path (file ).name
519- destination = hooks_dir / filename
520- download_file (url , destination )
521-
522521 # Add to settings
523522 if event not in settings ['hooks' ]:
524523 settings ['hooks' ][event ] = []
@@ -614,32 +613,14 @@ def create_launcher_script(claude_user_dir: Path, command_name: str, system_prom
614613 exit 1
615614}}
616615
617- # Build the bash command with all arguments
616+ # Call the shared script instead of building complex command
617+ $scriptPath = Join-Path $claudeUserDir "launch-{ command_name } .sh"
618+
618619if ($args.Count -gt 0) {{
619620 Write-Host "Passing additional arguments: $args" -ForegroundColor Cyan
620- # Properly escape arguments for bash (quote args with spaces)
621- $escapedArgs = @()
622- foreach ($arg in $args) {{
623- if ($arg -match ' ') {{
624- # If arg contains spaces, wrap in single quotes for bash
625- $escapedArgs += "'$arg'"
626- }} else {{
627- $escapedArgs += $arg
628- }}
629- }}
630- $argsString = $escapedArgs -join ' '
631- # Use a here-string to avoid complex quote escaping
632- $bashCommand = @"
633- p=`$(tr -d '\\ \\ r' < ~/.claude/prompts/{ system_prompt_file } ); s=~/.claude/additional-settings.json; \\
634- exec claude --append-system-prompt=\\ "`$p\\ " --settings=\\ "`$s\\ " $argsString
635- "@
636- & $bashPath -lc $bashCommand
621+ & $bashPath --login $scriptPath @args
637622}} else {{
638- # Use --% to stop PowerShell parsing, with literal command string
639- $bashCommand = "p=`$(tr -d '\\ r' < ~/.claude/prompts/{ system_prompt_file } ); " + `
640- "s=~/.claude/additional-settings.json; " + `
641- "exec claude --append-system-prompt=\\ `"`$p\\ `" --settings=\\ `"`$s\\ `""
642- & $bashPath --% -lc $bashCommand
623+ & $bashPath --login $scriptPath
643624}}
644625'''
645626 launcher_path .write_text (launcher_content )
@@ -660,19 +641,45 @@ def create_launcher_script(claude_user_dir: Path, command_name: str, system_prom
660641
661642echo Starting Claude Code with { command_name } configuration...
662643
663- REM Build the command with all arguments
664- set BASH_EXE="C:\\ Program Files\\ Git\\ bin\\ bash.exe"
665- set CMD_PREFIX=p=$(tr -d '\\ r' ^< ~/.claude/prompts/{ system_prompt_file } )
666- set SETTINGS_PATH=~/.claude/additional-settings.json
644+ REM Call shared script instead of complex command
645+ set "BASH_EXE=C:\\ Program Files\\ Git\\ bin\\ bash.exe"
646+ if not exist "%BASH_EXE%" set "BASH_EXE=C:\\ Program Files (x86)\\ Git\\ bin\\ bash.exe"
647+
648+ set "SCRIPT_WIN=%USERPROFILE%\\ .claude\\ launch-{ command_name } .sh"
649+
667650if "%~1"=="" (
668- %BASH_EXE% -lc "%CMD_PREFIX%; exec claude --append-system-prompt= \\ "$p \\ " --settings= \\ "%SETTINGS_PATH% \\ " "
651+ " %BASH_EXE%" --login "%SCRIPT_WIN% "
669652) else (
670653 echo Passing additional arguments: %*
671- %BASH_EXE% -lc "%CMD_PREFIX%; exec claude --append-system-prompt= \\ "$p \\ " --settings= \\ "%SETTINGS_PATH% \\ " %*"
654+ " %BASH_EXE%" --login "%SCRIPT_WIN% " %*
672655)
673656'''
674657 batch_path .write_text (batch_content )
675658
659+ # Create shared POSIX script that actually launches Claude
660+ shared_sh = claude_user_dir / f'launch-{ command_name } .sh'
661+ shared_sh_content = f'''#!/usr/bin/env bash
662+ set -euo pipefail
663+
664+ PROMPT_PATH="$HOME/.claude/prompts/{ system_prompt_file } "
665+ if [ ! -f "$PROMPT_PATH" ]; then
666+ echo "Error: System prompt not found at $PROMPT_PATH" >&2
667+ exit 1
668+ fi
669+
670+ # Read prompt and get Windows path for settings
671+ PROMPT_CONTENT=$(tr -d '\\ r' < "$PROMPT_PATH")
672+ # Try to get Windows path format; fallback to Unix path if cygpath is not available
673+ SETTINGS_WIN="$(cygpath -m "$HOME/.claude/additional-settings.json" 2>/dev/null ||
674+ echo "$HOME/.claude/additional-settings.json")"
675+
676+ exec claude --append-system-prompt "$PROMPT_CONTENT" --settings "$SETTINGS_WIN" "$@"
677+ '''
678+ shared_sh .write_text (shared_sh_content , newline = '\n ' )
679+ # Make it executable for bash
680+ with contextlib .suppress (Exception ):
681+ shared_sh .chmod (0o755 )
682+
676683 else :
677684 # Create bash launcher for Unix-like systems
678685 launcher_path = launcher_path .with_suffix ('.sh' )
@@ -731,13 +738,13 @@ def register_global_command(launcher_path: Path, command_name: str, system_promp
731738 batch_path = local_bin / f'{ command_name } .cmd'
732739 batch_content = f'''@echo off
733740REM Global { command_name } command for CMD
734- set BASH_EXE=" C:\\ Program Files\\ Git\\ bin\\ bash.exe"
735- set CMD_PREFIX=p=$(tr -d ' \\ r' ^< ~/.claude/prompts/ { system_prompt_file } )
736- set SETTINGS_PATH=~/ .claude/additional-settings.json
741+ set " BASH_EXE=C:\\ Program Files\\ Git\\ bin\\ bash.exe"
742+ if not exist "%BASH_EXE%" set "BASH_EXE=C: \\ Program Files (x86) \\ Git \\ bin \\ bash.exe"
743+ set "SCRIPT_WIN=%USERPROFILE% \\ .claude\\ launch- { command_name } .sh"
737744if "%~1"=="" (
738- %BASH_EXE% -lc "%CMD_PREFIX%; exec claude --append-system-prompt= \\ "$p \\ " --settings= \\ "%SETTINGS_PATH% \\ " "
745+ " %BASH_EXE%" --login "%SCRIPT_WIN% "
739746) else (
740- %BASH_EXE% -lc "%CMD_PREFIX%; exec claude --append-system-prompt= \\ "$p \\ " --settings= \\ "%SETTINGS_PATH% \\ " %*"
747+ " %BASH_EXE%" --login "%SCRIPT_WIN% " %*
741748)
742749'''
743750 batch_path .write_text (batch_content )
@@ -767,14 +774,14 @@ def register_global_command(launcher_path: Path, command_name: str, system_promp
767774
768775# Read the prompt content and pass it directly to claude
769776PROMPT_CONTENT=$(cat "$PROMPT_PATH")
770- SETTINGS_PATH ="$HOME/.claude/additional-settings.json"
777+ SETTINGS_WIN ="$(cygpath -m "$ HOME/.claude/additional-settings.json") "
771778
772779# Pass all arguments to claude with the system prompt
773780if [ $# -gt 0 ]; then
774781 echo "Passing additional arguments: $@"
775- claude --append-system-prompt "$PROMPT_CONTENT " --settings "$SETTINGS_PATH " "$@"
782+ claude --settings "$SETTINGS_WIN " --append-system-prompt "$PROMPT_CONTENT " "$@"
776783else
777- claude --append-system-prompt "$PROMPT_CONTENT " --settings "$SETTINGS_PATH "
784+ claude --settings "$SETTINGS_WIN " --append-system-prompt "$PROMPT_CONTENT "
778785fi
779786'''
780787 bash_wrapper_path .write_text (bash_content , newline = '\n ' ) # Use Unix line endings
@@ -931,11 +938,8 @@ def main() -> None:
931938 # Step 9: Configure hooks
932939 print ()
933940 print (f'{ Colors .CYAN } Step 9: Configuring hooks...{ Colors .NC } ' )
934- hooks = config .get ('hooks' , [])
935- if hooks :
936- create_additional_settings (hooks , claude_user_dir )
937- else :
938- info ('No hooks configured' )
941+ hooks = config .get ('hooks' , {})
942+ create_additional_settings (hooks , claude_user_dir )
939943
940944 # Step 10: Create launcher script
941945 print ()
@@ -966,7 +970,7 @@ def main() -> None:
966970 print (f' * Output styles: { len (output_styles ) if output_styles else 0 } installed' )
967971 print (' * System prompt: Configured' )
968972 print (f' * MCP servers: { len (mcp_servers )} configured' )
969- print (f' * Hooks: { len (hooks ) if hooks else 0 } configured' )
973+ print (f' * Hooks: { len (hooks . get ( "events" , []) ) if hooks else 0 } configured' )
970974 print (f' * Global command: { command_name } registered' )
971975
972976 print ()
0 commit comments