claude-mpm 3.1.2__py3-none-any.whl → 3.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. claude_mpm/__main__.py +17 -0
  2. claude_mpm/agents/INSTRUCTIONS.md +17 -2
  3. claude_mpm/cli/__init__.py +23 -14
  4. claude_mpm/cli/commands/agents.py +18 -7
  5. claude_mpm/cli/commands/info.py +10 -5
  6. claude_mpm/cli/commands/run.py +22 -7
  7. claude_mpm/cli/commands/tickets.py +17 -10
  8. claude_mpm/cli/commands/ui.py +37 -15
  9. claude_mpm/cli/utils.py +28 -9
  10. claude_mpm/core/agent_registry.py +4 -4
  11. claude_mpm/core/factories.py +1 -1
  12. claude_mpm/core/service_registry.py +1 -1
  13. claude_mpm/core/simple_runner.py +17 -27
  14. claude_mpm/hooks/claude_hooks/hook_handler.py +53 -4
  15. claude_mpm/models/__init__.py +91 -9
  16. claude_mpm/models/common.py +41 -0
  17. claude_mpm/models/lifecycle.py +97 -0
  18. claude_mpm/models/modification.py +126 -0
  19. claude_mpm/models/persistence.py +57 -0
  20. claude_mpm/models/registry.py +91 -0
  21. claude_mpm/security/__init__.py +8 -0
  22. claude_mpm/security/bash_validator.py +393 -0
  23. claude_mpm/services/agent_lifecycle_manager.py +25 -76
  24. claude_mpm/services/agent_modification_tracker.py +17 -98
  25. claude_mpm/services/agent_persistence_service.py +13 -33
  26. claude_mpm/services/agent_registry.py +43 -82
  27. claude_mpm/services/{ticketing_service_original.py → legacy_ticketing_service.py} +16 -9
  28. claude_mpm/services/ticket_manager.py +5 -4
  29. claude_mpm/services/{ticket_manager_di.py → ticket_manager_dependency_injection.py} +39 -12
  30. claude_mpm/services/version_control/semantic_versioning.py +10 -9
  31. claude_mpm/utils/path_operations.py +20 -0
  32. {claude_mpm-3.1.2.dist-info → claude_mpm-3.1.3.dist-info}/METADATA +9 -1
  33. {claude_mpm-3.1.2.dist-info → claude_mpm-3.1.3.dist-info}/RECORD +37 -31
  34. claude_mpm/utils/import_migration_example.py +0 -80
  35. {claude_mpm-3.1.2.dist-info → claude_mpm-3.1.3.dist-info}/WHEEL +0 -0
  36. {claude_mpm-3.1.2.dist-info → claude_mpm-3.1.3.dist-info}/entry_points.txt +0 -0
  37. {claude_mpm-3.1.2.dist-info → claude_mpm-3.1.3.dist-info}/licenses/LICENSE +0 -0
  38. {claude_mpm-3.1.2.dist-info → claude_mpm-3.1.3.dist-info}/top_level.txt +0 -0
claude_mpm/__main__.py CHANGED
@@ -8,6 +8,7 @@ DESIGN DECISION: We only import and call the main function from the CLI module,
8
8
  keeping this file minimal and focused on its single responsibility.
9
9
  """
10
10
 
11
+ import os
11
12
  import sys
12
13
  from pathlib import Path
13
14
 
@@ -17,5 +18,21 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
17
18
  # Import main function from the new CLI module structure
18
19
  from claude_mpm.cli import main
19
20
 
21
+ # Restore user's working directory if preserved by bash wrapper
22
+ # WHY: The bash wrapper preserves the user's launch directory in CLAUDE_MPM_USER_PWD
23
+ # to maintain proper file access permissions and security boundaries.
24
+ # Python imports work via PYTHONPATH, so we can safely restore the original directory.
20
25
  if __name__ == "__main__":
26
+ user_pwd = os.environ.get('CLAUDE_MPM_USER_PWD')
27
+ if user_pwd and os.path.exists(user_pwd):
28
+ try:
29
+ os.chdir(user_pwd)
30
+ # Only log if debug is enabled
31
+ if os.environ.get('CLAUDE_MPM_DEBUG') == '1':
32
+ print(f"[INFO] Restored working directory to: {user_pwd}")
33
+ except Exception as e:
34
+ # If we can't change to user directory, continue but log warning
35
+ if os.environ.get('CLAUDE_MPM_DEBUG') == '1':
36
+ print(f"[WARNING] Could not restore working directory to {user_pwd}: {e}")
37
+
21
38
  sys.exit(main())
@@ -4,15 +4,30 @@
4
4
  # Claude Multi-Agent Project Manager Instructions
5
5
 
6
6
  ## Core Identity & Authority
7
- You are **Claude Multi-Agent Project Manager (claude-mpm)** - your **SOLE function** is **orchestration and delegation**. You are **FORBIDDEN** from direct work except:
7
+ You are **Claude Multi-Agent Project Manager (claude-mpm)** - your **SOLE function** is **orchestration and delegation**.
8
+
9
+ ### Permitted Actions (Exhaustive List)
8
10
  - **Task Tool** for delegation (primary function)
9
11
  - **TodoWrite** for tracking (with [Agent] prefixes)
10
12
  - **WebSearch/WebFetch** only for delegation requirements
11
13
  - **Direct answers** for PM role/capability questions only
12
- - **Direct work** only when explicitly authorized: "do this yourself", "don't delegate", "implement directly"
14
+ - **Direct work** only when explicitly authorized with phrases like: "do this yourself", "don't delegate", "implement directly"
13
15
 
14
16
  **ABSOLUTE RULE**: ALL other work must be delegated to specialized agents via Task Tool.
15
17
 
18
+ ### Professional Standards
19
+ - **Clarity Threshold**: Achieve 90% certainty of success before delegating. Ask clarifying questions when needed.
20
+ - **Agent Standards**: Require the same 90% clarity threshold from your agents before they proceed.
21
+ - **Project Validation**: Verify that requested tasks align with project context and question any incongruities.
22
+ - **Communication Style**:
23
+ - Avoid unnecessary affirmation or praise
24
+ - Never say "You're right!" (or similar) unless correctness was in question and validated by agent authority
25
+ - Focus on substance over pleasantries
26
+ - Ask direct questions when clarity is needed
27
+
28
+ ### Your Responsibility
29
+ It is your **JOB** to ensure sufficient clarity before delegation. Set high standards for both yourself and your agents to ensure successful outcomes.
30
+
16
31
  ## Context-Aware Agent Selection
17
32
  - **PM role/capabilities questions**: Answer directly (only exception)
18
33
  - **Explanations/How-to questions**: Delegate to Documentation Agent
@@ -10,30 +10,35 @@ interface while organizing code into logical modules. The main() function
10
10
  remains the primary entry point for both direct execution and package imports.
11
11
  """
12
12
 
13
+ import os
13
14
  import sys
14
15
  from pathlib import Path
15
16
  from typing import Optional
16
17
 
17
- from ..constants import CLICommands, LogLevel
18
- from .parser import create_parser, preprocess_args
19
- from .utils import ensure_directories, setup_logging
20
- from .commands import (
21
- run_session,
22
- list_tickets,
23
- show_info,
24
- manage_agents,
25
- run_terminal_ui
26
- )
18
+ from claude_mpm.utils.imports import safe_import
19
+
20
+ # Import constants and utilities using safe_import pattern
21
+ CLICommands, LogLevel = safe_import('claude_mpm.constants', None, ['CLICommands', 'LogLevel'])
22
+ create_parser, preprocess_args = safe_import('claude_mpm.cli.parser', None, ['create_parser', 'preprocess_args'])
23
+ ensure_directories, setup_logging = safe_import('claude_mpm.cli.utils', None, ['ensure_directories', 'setup_logging'])
24
+
25
+ # Import commands
26
+ run_session = safe_import('claude_mpm.cli.commands', None, ['run_session'])
27
+ list_tickets = safe_import('claude_mpm.cli.commands', None, ['list_tickets'])
28
+ show_info = safe_import('claude_mpm.cli.commands', None, ['show_info'])
29
+ manage_agents = safe_import('claude_mpm.cli.commands', None, ['manage_agents'])
30
+ run_terminal_ui = safe_import('claude_mpm.cli.commands', None, ['run_terminal_ui'])
27
31
 
28
32
  # Get version from VERSION file - single source of truth
29
33
  version_file = Path(__file__).parent.parent.parent / "VERSION"
30
34
  if version_file.exists():
31
35
  __version__ = version_file.read_text().strip()
32
36
  else:
33
- # Try to import from package as fallback
34
- try:
35
- from .. import __version__
36
- except ImportError:
37
+ # Try to import from package as fallback using safe_import
38
+ version_module = safe_import('claude_mpm', None)
39
+ if version_module and hasattr(version_module, '__version__'):
40
+ __version__ = version_module.__version__
41
+ else:
37
42
  # Default version if all else fails
38
43
  __version__ = "0.0.0"
39
44
 
@@ -75,6 +80,10 @@ def main(argv: Optional[list] = None):
75
80
  if hasattr(args, 'debug') and args.debug:
76
81
  logger.debug(f"Command: {args.command}")
77
82
  logger.debug(f"Arguments: {args}")
83
+ # Log working directory context
84
+ logger.debug(f"Working directory: {os.getcwd()}")
85
+ logger.debug(f"User PWD (from env): {os.environ.get('CLAUDE_MPM_USER_PWD', 'Not set')}")
86
+ logger.debug(f"Framework path: {os.environ.get('CLAUDE_MPM_FRAMEWORK_PATH', 'Not set')}")
78
87
 
79
88
  # Hook system note: Claude Code hooks are handled externally via the
80
89
  # hook_handler.py script installed in ~/.claude/settings.json
@@ -7,9 +7,12 @@ and cleaning agent deployments.
7
7
 
8
8
  from pathlib import Path
9
9
 
10
- from ...core.logger import get_logger
11
- from ...constants import AgentCommands
12
- from ..utils import get_agent_versions_display
10
+ from claude_mpm.utils.imports import safe_import
11
+
12
+ # Import dependencies using safe_import pattern
13
+ get_logger = safe_import('claude_mpm.core.logger', None, ['get_logger'])
14
+ AgentCommands = safe_import('claude_mpm.constants', None, ['AgentCommands'])
15
+ get_agent_versions_display = safe_import('claude_mpm.cli.utils', None, ['get_agent_versions_display'])
13
16
 
14
17
 
15
18
  def manage_agents(args):
@@ -27,8 +30,19 @@ def manage_agents(args):
27
30
  """
28
31
  logger = get_logger("cli")
29
32
 
33
+ # Import AgentDeploymentService using safe_import pattern
34
+ AgentDeploymentService = safe_import(
35
+ 'claude_mpm.services.agent_deployment',
36
+ None,
37
+ ['AgentDeploymentService']
38
+ )
39
+
40
+ if not AgentDeploymentService:
41
+ logger.error("Agent deployment service not available")
42
+ print("Error: Agent deployment service not available")
43
+ return
44
+
30
45
  try:
31
- from ...services.agent_deployment import AgentDeploymentService
32
46
  deployment_service = AgentDeploymentService()
33
47
 
34
48
  if not args.agents_command:
@@ -55,9 +69,6 @@ def manage_agents(args):
55
69
  elif args.agents_command == AgentCommands.CLEAN.value:
56
70
  _clean_agents(args, deployment_service)
57
71
 
58
- except ImportError:
59
- logger.error("Agent deployment service not available")
60
- print("Error: Agent deployment service not available")
61
72
  except Exception as e:
62
73
  logger.error(f"Error managing agents: {e}")
63
74
  print(f"Error: {e}")
@@ -8,7 +8,10 @@ users understand their claude-mpm setup and troubleshoot issues.
8
8
  import shutil
9
9
  from pathlib import Path
10
10
 
11
- from ...core.logger import get_logger
11
+ from claude_mpm.utils.imports import safe_import
12
+
13
+ # Import logger using safe_import pattern
14
+ get_logger = safe_import('claude_mpm.core.logger', None, ['get_logger'])
12
15
 
13
16
 
14
17
  def show_info(args):
@@ -25,10 +28,12 @@ def show_info(args):
25
28
  Args:
26
29
  args: Parsed command line arguments
27
30
  """
28
- try:
29
- from ...core.framework_loader import FrameworkLoader
30
- except ImportError:
31
- from claude_mpm.core.framework_loader import FrameworkLoader
31
+ # Import FrameworkLoader using safe_import pattern
32
+ FrameworkLoader = safe_import(
33
+ 'claude_mpm.core.framework_loader',
34
+ None,
35
+ ['FrameworkLoader']
36
+ )
32
37
 
33
38
  print("Claude MPM - Multi-Agent Project Manager")
34
39
  print("=" * 50)
@@ -9,9 +9,16 @@ import subprocess
9
9
  import sys
10
10
  from pathlib import Path
11
11
 
12
- from ...core.logger import get_logger
13
- from ...constants import LogLevel
14
- from ..utils import get_user_input, list_agent_versions_at_startup
12
+ from claude_mpm.utils.imports import safe_import
13
+
14
+ # Import with safe_import pattern for better error handling
15
+ get_logger = safe_import('claude_mpm.core.logger', None, ['get_logger'])
16
+ LogLevel = safe_import('claude_mpm.constants', None, ['LogLevel'])
17
+ get_user_input, list_agent_versions_at_startup = safe_import(
18
+ 'claude_mpm.cli.utils',
19
+ None,
20
+ ['get_user_input', 'list_agent_versions_at_startup']
21
+ )
15
22
 
16
23
 
17
24
  def run_session(args):
@@ -28,14 +35,22 @@ def run_session(args):
28
35
  Args:
29
36
  args: Parsed command line arguments
30
37
  """
38
+ import os
31
39
  logger = get_logger("cli")
32
40
  if args.logging != LogLevel.OFF.value:
33
41
  logger.info("Starting Claude MPM session")
42
+ # Log working directory context
43
+ logger.info(f"Working directory: {os.getcwd()}")
44
+ if args.debug:
45
+ logger.debug(f"User PWD (from env): {os.environ.get('CLAUDE_MPM_USER_PWD', 'Not set')}")
46
+ logger.debug(f"Framework path: {os.environ.get('CLAUDE_MPM_FRAMEWORK_PATH', 'Not set')}")
34
47
 
35
- try:
36
- from ...core.simple_runner import SimpleClaudeRunner, create_simple_context
37
- except ImportError:
38
- from claude_mpm.core.simple_runner import SimpleClaudeRunner, create_simple_context
48
+ # Import SimpleClaudeRunner using safe_import pattern
49
+ SimpleClaudeRunner, create_simple_context = safe_import(
50
+ 'claude_mpm.core.simple_runner',
51
+ None,
52
+ ['SimpleClaudeRunner', 'create_simple_context']
53
+ )
39
54
 
40
55
  # Skip native agents if disabled
41
56
  if getattr(args, 'no_native_agents', False):
@@ -5,7 +5,10 @@ WHY: This module handles ticket listing functionality, allowing users to view
5
5
  recent tickets created during Claude sessions.
6
6
  """
7
7
 
8
- from ...core.logger import get_logger
8
+ from claude_mpm.utils.imports import safe_import
9
+
10
+ # Import logger using safe_import pattern
11
+ get_logger = safe_import('claude_mpm.core.logger', None, ['get_logger'])
9
12
 
10
13
 
11
14
  def list_tickets(args):
@@ -24,12 +27,20 @@ def list_tickets(args):
24
27
  """
25
28
  logger = get_logger("cli")
26
29
 
30
+ # Import TicketManager using safe_import pattern
31
+ TicketManager = safe_import(
32
+ '...services.ticket_manager',
33
+ 'claude_mpm.services.ticket_manager',
34
+ ['TicketManager']
35
+ )
36
+
37
+ if not TicketManager:
38
+ logger.error("ai-trackdown-pytools not installed")
39
+ print("Error: ai-trackdown-pytools not installed")
40
+ print("Install with: pip install ai-trackdown-pytools")
41
+ return
42
+
27
43
  try:
28
- try:
29
- from ...services.ticket_manager import TicketManager
30
- except ImportError:
31
- from claude_mpm.services.ticket_manager import TicketManager
32
-
33
44
  ticket_manager = TicketManager()
34
45
  tickets = ticket_manager.list_recent_tickets(limit=args.limit)
35
46
 
@@ -54,10 +65,6 @@ def list_tickets(args):
54
65
  print(f" Created: {ticket['created_at']}")
55
66
  print()
56
67
 
57
- except ImportError:
58
- logger.error("ai-trackdown-pytools not installed")
59
- print("Error: ai-trackdown-pytools not installed")
60
- print("Install with: pip install ai-trackdown-pytools")
61
68
  except Exception as e:
62
69
  logger.error(f"Error listing tickets: {e}")
63
70
  print(f"Error: {e}")
@@ -5,7 +5,10 @@ WHY: This module provides terminal UI functionality for users who prefer a
5
5
  visual interface over command-line interaction.
6
6
  """
7
7
 
8
- from ...core.logger import get_logger
8
+ from claude_mpm.utils.imports import safe_import
9
+
10
+ # Import logger using safe_import pattern
11
+ get_logger = safe_import('claude_mpm.core.logger', None, ['get_logger'])
9
12
 
10
13
 
11
14
  def run_terminal_ui(args):
@@ -29,26 +32,45 @@ def run_terminal_ui(args):
29
32
 
30
33
  try:
31
34
  if ui_mode == 'terminal':
32
- # Try rich UI first
33
- try:
34
- from ...ui.rich_terminal_ui import main as run_rich_ui
35
+ # Try rich UI first using safe_import
36
+ run_rich_ui = safe_import(
37
+ '...ui.rich_terminal_ui',
38
+ 'claude_mpm.ui.rich_terminal_ui',
39
+ ['main']
40
+ )
41
+
42
+ if run_rich_ui:
35
43
  logger.info("Starting rich terminal UI...")
36
44
  run_rich_ui()
37
- except ImportError:
45
+ else:
38
46
  # Fallback to curses UI
39
47
  logger.info("Rich not available, falling back to curses UI...")
40
- from ...ui.terminal_ui import TerminalUI
41
- ui = TerminalUI()
42
- ui.run()
48
+ TerminalUI = safe_import(
49
+ '...ui.terminal_ui',
50
+ 'claude_mpm.ui.terminal_ui',
51
+ ['TerminalUI']
52
+ )
53
+ if TerminalUI:
54
+ ui = TerminalUI()
55
+ ui.run()
56
+ else:
57
+ logger.error("UI module not found")
58
+ print("Error: Terminal UI requires 'curses' (built-in) or 'rich' (pip install rich)")
59
+ return 1
43
60
  else:
44
61
  # Use curses UI explicitly
45
- from ...ui.terminal_ui import TerminalUI
46
- ui = TerminalUI()
47
- ui.run()
48
- except ImportError as e:
49
- logger.error(f"UI module not found: {e}")
50
- print(f"Error: Terminal UI requires 'curses' (built-in) or 'rich' (pip install rich)")
51
- return 1
62
+ TerminalUI = safe_import(
63
+ '...ui.terminal_ui',
64
+ 'claude_mpm.ui.terminal_ui',
65
+ ['TerminalUI']
66
+ )
67
+ if TerminalUI:
68
+ ui = TerminalUI()
69
+ ui.run()
70
+ else:
71
+ logger.error("UI module not found")
72
+ print("Error: Terminal UI not available")
73
+ return 1
52
74
  except Exception as e:
53
75
  logger.error(f"Error running terminal UI: {e}")
54
76
  print(f"Error: {e}")
claude_mpm/cli/utils.py CHANGED
@@ -10,7 +10,10 @@ import sys
10
10
  from pathlib import Path
11
11
  from typing import Optional
12
12
 
13
- from ..core.logger import get_logger
13
+ from claude_mpm.utils.imports import safe_import
14
+
15
+ # Import logger using safe_import pattern
16
+ get_logger = safe_import('claude_mpm.core.logger', None, ['get_logger'])
14
17
 
15
18
 
16
19
  def get_user_input(input_arg: Optional[str], logger) -> str:
@@ -59,8 +62,17 @@ def get_agent_versions_display() -> Optional[str]:
59
62
  Returns:
60
63
  Formatted string containing agent version information, or None if failed
61
64
  """
65
+ # Import AgentDeploymentService using safe_import pattern
66
+ AgentDeploymentService = safe_import(
67
+ 'claude_mpm.services.agent_deployment',
68
+ None,
69
+ ['AgentDeploymentService']
70
+ )
71
+
72
+ if not AgentDeploymentService:
73
+ return None
74
+
62
75
  try:
63
- from ..services.agent_deployment import AgentDeploymentService
64
76
  deployment_service = AgentDeploymentService()
65
77
 
66
78
  # Get deployed agents
@@ -162,10 +174,17 @@ def ensure_directories() -> None:
162
174
  failing when they don't exist, we create them automatically for a better
163
175
  user experience.
164
176
  """
165
- try:
166
- from ..init import ensure_directories as init_ensure_directories
167
- init_ensure_directories()
168
- except Exception:
169
- # Continue even if initialization fails
170
- # The individual commands will handle missing directories as needed
171
- pass
177
+ # Import ensure_directories using safe_import pattern
178
+ init_ensure_directories = safe_import(
179
+ 'claude_mpm.init',
180
+ None,
181
+ ['ensure_directories']
182
+ )
183
+
184
+ if init_ensure_directories:
185
+ try:
186
+ init_ensure_directories()
187
+ except Exception:
188
+ # Continue even if initialization fails
189
+ # The individual commands will handle missing directories as needed
190
+ pass
@@ -20,10 +20,10 @@ import importlib.util
20
20
  from datetime import datetime
21
21
  from dataclasses import dataclass
22
22
 
23
- try:
24
- from ..core.logger import get_logger
25
- except ImportError:
26
- from core.logger import get_logger
23
+ from claude_mpm.utils.imports import safe_import
24
+
25
+ # Import logger using safe_import pattern
26
+ get_logger = safe_import('claude_mpm.core.logger', None, ['get_logger'])
27
27
 
28
28
 
29
29
  @dataclass
@@ -12,7 +12,7 @@ from typing import Any, Dict, Optional, Type, TypeVar
12
12
  from .container import DIContainer
13
13
  from .logger import get_logger
14
14
  from .config import Config
15
- from ..services.agent_deployment import AgentDeploymentService
15
+ from claude_mpm.services.agent_deployment import AgentDeploymentService
16
16
  from ..orchestration.factory import OrchestratorFactory
17
17
  from ..orchestration.base import BaseOrchestrator
18
18
 
@@ -38,7 +38,7 @@ class ServiceRegistry:
38
38
  """Register all core framework services."""
39
39
  from ..services.shared_prompt_cache import SharedPromptCache
40
40
  from ..services.ticket_manager import TicketManager
41
- from ..services.agent_deployment import AgentDeploymentService
41
+ from claude_mpm.services.agent_deployment import AgentDeploymentService
42
42
  from .session_manager import SessionManager
43
43
  from .agent_session_manager import AgentSessionManager
44
44
  from .config import Config
@@ -195,16 +195,15 @@ class SimpleClaudeRunner:
195
195
  clean_env.pop(var, None)
196
196
 
197
197
  # Set the correct working directory for Claude Code
198
- # If CLAUDE_MPM_USER_PWD is set, use that as the working directory
198
+ # WHY: We're already in the user's directory thanks to __main__.py restoration
199
+ # We just need to ensure CLAUDE_WORKSPACE is set correctly
200
+ current_dir = os.getcwd()
201
+ clean_env['CLAUDE_WORKSPACE'] = current_dir
202
+ self.logger.info(f"Working directory: {current_dir}")
203
+
204
+ # Log directory context for debugging
199
205
  if 'CLAUDE_MPM_USER_PWD' in clean_env:
200
- user_pwd = clean_env['CLAUDE_MPM_USER_PWD']
201
- clean_env['CLAUDE_WORKSPACE'] = user_pwd
202
- # Also change to that directory before launching Claude
203
- try:
204
- os.chdir(user_pwd)
205
- self.logger.info(f"Changed working directory to: {user_pwd}")
206
- except Exception as e:
207
- self.logger.warning(f"Could not change to user directory {user_pwd}: {e}")
206
+ self.logger.debug(f"User PWD from env: {clean_env['CLAUDE_MPM_USER_PWD']}")
208
207
 
209
208
  print("Launching Claude...")
210
209
 
@@ -313,19 +312,15 @@ class SimpleClaudeRunner:
313
312
  env = os.environ.copy()
314
313
 
315
314
  # Set the correct working directory for Claude Code
315
+ # WHY: We're already in the user's directory thanks to __main__.py restoration
316
+ # We just need to ensure CLAUDE_WORKSPACE is set correctly
317
+ current_dir = os.getcwd()
318
+ env['CLAUDE_WORKSPACE'] = current_dir
319
+ self.logger.info(f"Working directory for subprocess: {current_dir}")
320
+
321
+ # Log directory context for debugging
316
322
  if 'CLAUDE_MPM_USER_PWD' in env:
317
- user_pwd = env['CLAUDE_MPM_USER_PWD']
318
- env['CLAUDE_WORKSPACE'] = user_pwd
319
- # Change to that directory before running Claude
320
- try:
321
- original_cwd = os.getcwd()
322
- os.chdir(user_pwd)
323
- self.logger.info(f"Changed working directory to: {user_pwd}")
324
- except Exception as e:
325
- self.logger.warning(f"Could not change to user directory {user_pwd}: {e}")
326
- original_cwd = None
327
- else:
328
- original_cwd = None
323
+ self.logger.debug(f"User PWD from env: {env['CLAUDE_MPM_USER_PWD']}")
329
324
 
330
325
  # Run Claude
331
326
  if self.project_logger:
@@ -337,12 +332,7 @@ class SimpleClaudeRunner:
337
332
 
338
333
  result = subprocess.run(cmd, capture_output=True, text=True, env=env)
339
334
 
340
- # Restore original directory if we changed it
341
- if original_cwd:
342
- try:
343
- os.chdir(original_cwd)
344
- except Exception:
345
- pass
335
+ # No need to restore directory since we didn't change it
346
336
  execution_time = time.time() - start_time
347
337
 
348
338
  if result.returncode == 0:
@@ -44,6 +44,7 @@ project_root = Path(__file__).parent.parent.parent.parent
44
44
  sys.path.insert(0, str(project_root / 'src'))
45
45
 
46
46
  from claude_mpm.core.logger import get_logger, setup_logging, LogLevel
47
+ from claude_mpm.security import BashSecurityValidator
47
48
 
48
49
  # Don't initialize global logging here - we'll do it per-project
49
50
  logger = None
@@ -329,6 +330,7 @@ class ClaudeHookHandler:
329
330
  - Checking for path traversal attempts
330
331
  - Ensuring file operations stay within the working directory
331
332
  - Blocking dangerous operations
333
+ - Validating bash commands for security violations
332
334
 
333
335
  Security Design:
334
336
  - Fail-secure: Block suspicious operations
@@ -342,6 +344,29 @@ class ClaudeHookHandler:
342
344
  tool_name = self.event.get('tool_name', '')
343
345
  tool_input = self.event.get('tool_input', {})
344
346
 
347
+ # Get the working directory from the event
348
+ working_dir = Path(self.event.get('cwd', os.getcwd())).resolve()
349
+
350
+ # Special handling for Bash tool - validate commands for security
351
+ if tool_name == 'Bash':
352
+ command = tool_input.get('command', '')
353
+ if command:
354
+ # Create bash validator for this working directory
355
+ bash_validator = BashSecurityValidator(working_dir)
356
+ is_valid, error_msg = bash_validator.validate_command(command)
357
+
358
+ if not is_valid:
359
+ if logger:
360
+ logger.warning(f"Security: Blocked Bash command: {command[:100]}...")
361
+
362
+ response = {
363
+ "action": "block",
364
+ "error": error_msg
365
+ }
366
+ print(json.dumps(response))
367
+ sys.exit(0)
368
+ return
369
+
345
370
  # List of tools that perform write operations
346
371
  # These tools need special security checks to prevent
347
372
  # writing outside the project directory
@@ -349,9 +374,6 @@ class ClaudeHookHandler:
349
374
 
350
375
  # Check if this is a write operation
351
376
  if tool_name in write_tools:
352
- # Get the working directory from the event
353
- working_dir = Path(self.event.get('cwd', os.getcwd())).resolve()
354
-
355
377
  # Extract file path based on tool type
356
378
  file_path = None
357
379
  if tool_name in ['Write', 'Edit', 'NotebookEdit']:
@@ -414,7 +436,34 @@ class ClaudeHookHandler:
414
436
  sys.exit(0)
415
437
  return
416
438
 
417
- # For read operations and other tools, continue normally
439
+ # Additional security checks for other tools that might access files
440
+ read_tools = ['Read', 'LS', 'Glob', 'Grep', 'NotebookRead']
441
+
442
+ if tool_name in read_tools:
443
+ # For read operations, we allow them but log for auditing
444
+ file_path = None
445
+ if tool_name == 'Read':
446
+ file_path = tool_input.get('file_path')
447
+ elif tool_name == 'NotebookRead':
448
+ file_path = tool_input.get('notebook_path')
449
+ elif tool_name == 'LS':
450
+ file_path = tool_input.get('path')
451
+ elif tool_name in ['Glob', 'Grep']:
452
+ file_path = tool_input.get('path', '.')
453
+
454
+ if file_path and logger:
455
+ # Log read operations for security auditing
456
+ logger.info(f"Security audit: {tool_name} accessing path: {file_path}")
457
+
458
+ # Check for other potentially dangerous tools
459
+ if tool_name in ['WebFetch', 'WebSearch']:
460
+ # Log web access for security auditing
461
+ if logger:
462
+ url = tool_input.get('url', '') if tool_name == 'WebFetch' else 'N/A'
463
+ query = tool_input.get('query', '') if tool_name == 'WebSearch' else 'N/A'
464
+ logger.info(f"Security audit: {tool_name} - URL: {url}, Query: {query}")
465
+
466
+ # For all other operations, continue normally
418
467
  return self._continue()
419
468
 
420
469
  def _handle_post_tool_use(self):