Skip to content

Feature or Bug?: Allow custom hooks to be loaded from ~/.ccproxy directory #9

@npinto

Description

@npinto

Feature Request: Allow custom hooks to be loaded from ~/.ccproxy directory

Summary

Custom hooks defined in ccproxy.yaml cannot be loaded from Python files in ~/.ccproxy/ without workarounds, even though LiteLLM callbacks in config.yaml can load adjacent scripts automatically.

Current Behavior

When I create a custom hook file at ~/.ccproxy/custom_hooks.py:

# ~/.ccproxy/custom_hooks.py
def forward_oauth_dynamic(data, user_api_key_dict, **kwargs):
    # Custom OAuth logic
    return data

And reference it in ~/.ccproxy/ccproxy.yaml:

ccproxy:
  hooks:
    - ccproxy.hooks.rule_evaluator
    - ccproxy.hooks.model_router
    - custom_hooks.forward_oauth_dynamic  # <-- FAILS

The proxy fails to start with:

ImportError: No module named 'custom_hooks'

Expected Behavior

Custom hooks should load from ~/.ccproxy/ the same way LiteLLM callbacks do.

For comparison, this works in ~/.ccproxy/config.yaml:

litellm_settings:
  callbacks:
    - jsonl_logger.jsonl_logger.handler  # Loads from ~/.ccproxy/jsonl_logger/

LiteLLM successfully imports jsonl_logger from the config directory because it adds the config directory to sys.path before importing callbacks.

Root Cause

The load_hooks() method in src/ccproxy/config.py uses importlib.import_module() directly without adding the config directory to sys.path:

def load_hooks(self) -> list[tuple[Any, dict[str, Any]]]:
    # ...
    module = importlib.import_module(module_path)  # Fails for adjacent scripts

Proposed Solution

Add the config directory to sys.path before importing hooks, matching LiteLLM's behavior:

import sys

def load_hooks(self) -> list[tuple[Any, dict[str, Any]]]:
    """Load hook functions from their import paths."""
    # Add config directory to sys.path for custom hooks
    config_dir = str(Path.home() / ".ccproxy")
    if config_dir not in sys.path:
        sys.path.insert(0, config_dir)

    loaded_hooks = []
    for hook_entry in self.hooks:
        # ... existing code ...

Current Workarounds

  1. Copy to package directory (fragile, lost on upgrade):

    cp ~/.ccproxy/custom_hooks.py ~/.local/share/uv/tools/claude-ccproxy/lib/python3.11/site-packages/ccproxy/

    Then use ccproxy.custom_hooks.forward_oauth_dynamic in yaml.

  2. Set PYTHONPATH (requires environment setup):

    export PYTHONPATH="$HOME/.ccproxy:$PYTHONPATH"

Both workarounds are inconvenient compared to LiteLLM's seamless adjacent-file loading.

Documentation Reference

The README states:

Custom rules (and hooks) are loaded with the same mechanism that LiteLLM uses to import the custom callbacks - they are imported by the LiteLLM python process as named module from within its virtual environment, or as a python script adjacent to config.yaml.

This suggests adjacent-file loading should work, but currently only the "named module from virtual environment" path works for ccproxy hooks.

Environment

  • ccproxy version: 1.2.0
  • Python: 3.11
  • OS: macOS
  • Installation method: uv tool install claude-ccproxy

Related

  • LiteLLM callback loading works correctly for adjacent files
  • This affects anyone wanting to write custom hooks without modifying the installed package

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