Skip to main content

A Redux Saga-like effect system for Python, designed for Claude Code hooks

Project description

Claude Saga

A side-effect manager for Python scripts, specifically designed for building maintainable (easy to build, test, debug) Claude Code hooks, inspired by Redux Saga.

Disclaimer:

Unstable - API subject to change.

Quick Start

Conceptual overview

from claude_saga import (
    BaseSagaState, SagaRuntime,
    Call, Put, Select, Log, Stop, Complete,
    run_command_effect
)

def add(x, y):
    return x + y

class State(BaseSagaState):
    math_result: int = 2
    command_result: str = ""

def my_saga():
    yield Log("info", "Starting saga")
    initial_state = yield Select()
    math_result = yield Call(add, initial_state.math_result, 3)
    command_result = yield Call(run_command_effect, "echo 'Hello World'")
    if command_result is None:
        yield Log("error", "unable to run command")
        yield Stop("hook failed, exited early")
    yield Put({"command_result": command_result.stdout, "math_result": math_result})
    yield Complete("Saga completed successfully")

runtime = SagaRuntime(State())
final_state = runtime.run(my_saga())
print(final_state.to_json())

Building Claude Code Hooks

Claude Saga handles input/output conventions of claude code hooks:

#!/usr/bin/env python
import json
import sys
from claude_saga import (
    BaseSagaState, SagaRuntime,
    validate_input_saga, parse_json_saga,
    Complete
)

class HookState(BaseSagaState):
    # Add your custom state fields
    pass

def main_saga():
    # Validate and parse input
    # https://docs.anthropic.com/en/docs/claude-code/hooks#hook-input
    yield from validate_input_saga()
    # Adds input data to state
    yield from parse_json_saga()
    
    # Your hook logic here
    
    # Complete
    yield Complete("Hook executed successfully")

def main():
    runtime = SagaRuntime(HookState())
    # Final state is an object that conforms to common json fields:
    # https://docs.anthropic.com/en/docs/claude-code/hooks#common-json-fields
    final_state = runtime.run(main_saga())
    # Claude Code exit code behavior:
    # https://docs.anthropic.com/en/docs/claude-code/hooks#simple%3A-exit-code
    print(json.dumps(final_state.to_json()))
    sys.exit(0 if final_state.continue_ else 1)

if __name__ == "__main__":
    main()

Effect Types

Call

Execute functions, including(and especially) those with side-effects:

result = yield Call(function, arg1, arg2, kwarg=value)

Put

Update the state:

yield Put({"field": "value"})
# or with a function
yield Put(lambda state: MyState(counter=state.counter + 1))

Select

Read from the state:

state = yield Select()
# or with a selector
counter = yield Select(lambda state: state.counter)

Log

Log messages at different levels:

yield Log("info", "Information message")
yield Log("error", "Error message")
yield Log("debug", "Debug message")  # Only shown with DEBUG=1

Stop

Stop execution with an error, hook output contains continue:false:

yield Stop("Error message")

Complete

Complete saga successfully, hook output contains continue:true:

yield Complete("Success message")

Common Effects

The library includes common effects

  • log_info, log_error, log_debug
  • run_command_effect(cmd, cwd=None, capture_output=True) - Run shell commands
  • write_file_effect(path, content) - Write files
  • change_directory_effect(path) - Change working directory
  • create_directory_effect(path) - Create directories
  • connect_pycharm_debugger_effect() - Connect to PyCharm debugger

Notes:

  • When you write your own effects - you don't need to implement error handling - the saga runtime handles Call errors (logs them to stdout) and returns None on failure.
    • If you want to terminate the saga on effect failure, check if the Call result is None and yield a Stop.

Common Sagas

Pre-built sagas for common tasks:

  • validate_input_saga() - Validate stdin input is provided
  • parse_json_saga() - Parse JSON from stdin into hook state (parses specifically for Claude Code hook input)

Development

Setup

uv pip install -e .

Examples

The examples/ directory contains a practical demonstration:

  • simple_command_validator.py - Claude Code hook for validating bash commands (saga version of the official example)
# This will fail since the expected input to stdin is not provided
uv run examples/simple_command_validator.py

# Handle claude code stdin conventions, this command passes validation 
echo '{"tool_name": "Bash", "tool_input": {"command": "ls -la"}}' | uv run examples/simple_command_validator.py

# This command fails validation (uses grep instead of rg)
echo '{"tool_name": "Bash", "tool_input": {"command": "grep pattern file.txt"}}' | uv run examples/simple_command_validator.py

Running Tests

Unit Tests

Test the core saga framework components:

uv run pytest tests/test_claude_saga.py -v

E2E Tests

Test complete example hook behavior:

uv run pytest tests/test_e2e_simple_command_validator.py -v

All Tests

Run the complete test suite:

uv run pytest tests/ -v

Building

uv build

License

MIT License - see LICENSE file for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request. I'd like to hear what common effects can be added to the core lib. e.g.

  • http_request_effect
  • mcp_request_effect

Future work must incorporate

  • parsing & validation for each hook's unique input/output behaviors, fields etc...
  • retry-able effects
  • cancel-able effects
  • parallel effects (e.g. All), see hypothetical async effects like mcp_request etc...
  • concurrent effects

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

claude_saga-0.1.2.tar.gz (11.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

claude_saga-0.1.2-py3-none-any.whl (7.7 kB view details)

Uploaded Python 3

File details

Details for the file claude_saga-0.1.2.tar.gz.

File metadata

  • Download URL: claude_saga-0.1.2.tar.gz
  • Upload date:
  • Size: 11.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for claude_saga-0.1.2.tar.gz
Algorithm Hash digest
SHA256 f011da85bc329969032853304d9a013685854bac3ffdf3209d17c1bcae404431
MD5 025417ba9b994b7b41482eb562e09c41
BLAKE2b-256 8b8f33283e2b8cdc2a6ee5dd5ba15487d23a97b6c4a354b2113309cdc8a4148f

See more details on using hashes here.

Provenance

The following attestation bundles were made for claude_saga-0.1.2.tar.gz:

Publisher: publish.yml on listfold/claude-saga

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file claude_saga-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: claude_saga-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 7.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for claude_saga-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 b2d8ecc0920c03dc9bad23d06295d9f46f8a26b7c42b2a3a6c46f817a8dae88e
MD5 2544171e18fd537829e180c899dc7189
BLAKE2b-256 125c6f87726b415f46d8e413a4105c09c858f5d96108dbdf823c47181abfe114

See more details on using hashes here.

Provenance

The following attestation bundles were made for claude_saga-0.1.2-py3-none-any.whl:

Publisher: publish.yml on listfold/claude-saga

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page