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_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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file claude_saga-0.1.1.tar.gz.
File metadata
- Download URL: claude_saga-0.1.1.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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bfa4ff4f60d4ce947db83f1442a4f973065cc77c5745e8c249590c7fb0bb6c49
|
|
| MD5 |
06436ca2719cebcef5f5e010c3ca490d
|
|
| BLAKE2b-256 |
e96d131826d6a29891a760f21323658b5be26378e78ba78560001fb4a4961afd
|
Provenance
The following attestation bundles were made for claude_saga-0.1.1.tar.gz:
Publisher:
publish.yml on listfold/claude-saga
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
claude_saga-0.1.1.tar.gz -
Subject digest:
bfa4ff4f60d4ce947db83f1442a4f973065cc77c5745e8c249590c7fb0bb6c49 - Sigstore transparency entry: 485638297
- Sigstore integration time:
-
Permalink:
listfold/claude-saga@a7d84943aa37ede9126c4e6235a660a13402d883 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/listfold
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a7d84943aa37ede9126c4e6235a660a13402d883 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file claude_saga-0.1.1-py3-none-any.whl.
File metadata
- Download URL: claude_saga-0.1.1-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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cd6501bd22d48e958357fa436a2dc38296e67c56b38637a80783e924982f462e
|
|
| MD5 |
1408976da79f615b5ce8386caa1b343b
|
|
| BLAKE2b-256 |
6f32617800aa020172d6a00be5d345b64dd02fce32c086a59eb2cf1937c05c4c
|
Provenance
The following attestation bundles were made for claude_saga-0.1.1-py3-none-any.whl:
Publisher:
publish.yml on listfold/claude-saga
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
claude_saga-0.1.1-py3-none-any.whl -
Subject digest:
cd6501bd22d48e958357fa436a2dc38296e67c56b38637a80783e924982f462e - Sigstore transparency entry: 485638322
- Sigstore integration time:
-
Permalink:
listfold/claude-saga@a7d84943aa37ede9126c4e6235a660a13402d883 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/listfold
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a7d84943aa37ede9126c4e6235a660a13402d883 -
Trigger Event:
workflow_dispatch
-
Statement type: