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_debugrun_command_effect(cmd, cwd=None, capture_output=True)- Run shell commandswrite_file_effect(path, content)- Write fileschange_directory_effect(path)- Change working directorycreate_directory_effect(path)- Create directoriesconnect_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
Noneon failure.- If you want to terminate the saga on effect failure, check if the Call result is
Noneand yield aStop.
- If you want to terminate the saga on effect failure, check if the Call result is
Common Sagas
Pre-built sagas for common tasks:
validate_input_saga()- Validate stdin input is providedparse_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 likemcp_requestetc... - concurrent effects
Metadata
Release files for claude-saga 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| claude_saga-0.1.2.tar.gz | 11.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| claude_saga-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 18.8 kB
Release files / claude_saga-0.1.2.tar.gz
| Download URL | claude_saga-0.1.2.tar.gz |
|---|---|
| Size | 11.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f011da85bc329969032853304d9a013685854bac3ffdf3209d17c1bcae404431
|
|
BLAKE2b-256 checksum How to use checksums |
8b8f33283e2b8cdc2a6ee5dd5ba15487d23a97b6c4a354b2113309cdc8a4148f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 10, 2025.
Transparency logRelease files / claude_saga-0.1.2-py3-none-any.whl
| Download URL | claude_saga-0.1.2-py3-none-any.whl |
|---|---|
| Size | 7.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b2d8ecc0920c03dc9bad23d06295d9f46f8a26b7c42b2a3a6c46f817a8dae88e
|
|
BLAKE2b-256 checksum How to use checksums |
125c6f87726b415f46d8e413a4105c09c858f5d96108dbdf823c47181abfe114
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 10, 2025.
Transparency log