# Claude Saga
A side-effect manager for Python scripts, specifically designed for building maintainable (easy to build, test, debug) [Claude Code hooks](https://docs.anthropic.com/en/docs/claude-code/hooks), inspired by [Redux Saga](https://redux-saga.js.org/).
#### Disclaimer:
Unstable - API subject to change.
## Quick Start
### Conceptual overview
```python
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:
```python
#!/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:
```python
result = yield Call(function, arg1, arg2, kwarg=value)
```
### Put
Update the state:
```python
yield Put({"field": "value"})
# or with a function
yield Put(lambda state: MyState(counter=state.counter + 1))
```
### Select
Read from the state:
```python
state = yield Select()
# or with a selector
counter = yield Select(lambda state: state.counter)
```
### Log
Log messages at different levels:
```python
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`:
```python
yield Stop("Error message")
```
### Complete
Complete saga successfully, hook output contains `continue:true`:
```python
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](https://docs.anthropic.com/en/docs/claude-code/hooks#hook-input))
# Development
### Setup
```bash
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](https://docs.anthropic.com/en/docs/claude-code/hooks#exit-code-example%3A-bash-command-validation))
```bash
# 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:
```bash
uv run pytest tests/test_claude_saga.py -v
```
#### E2E Tests
Test complete example hook behavior:
```bash
uv run pytest tests/test_e2e_simple_command_validator.py -v
```
#### All Tests
Run the complete test suite:
```bash
uv run pytest tests/ -v
```
### Building
```bash
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
Raw data
{
"_id": null,
"home_page": null,
"name": "claude-saga",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.12",
"maintainer_email": null,
"keywords": "claude, effects, functional, generator, hooks, saga, side-effects",
"author": "Iain",
"author_email": null,
"download_url": "https://files.pythonhosted.org/packages/6e/1e/7db10ea88d646cd0e39a5fb8c502b323c0fc80302affae9d62dfd3fb69dd/claude_saga-0.1.0.tar.gz",
"platform": null,
"description": "# Claude Saga\nA side-effect manager for Python scripts, specifically designed for building maintainable (easy to build, test, debug) [Claude Code hooks](https://docs.anthropic.com/en/docs/claude-code/hooks), inspired by [Redux Saga](https://redux-saga.js.org/).\n\n#### Disclaimer:\nUnstable - API subject to change.\n\n## Quick Start\n### Conceptual overview\n```python\nfrom claude_saga import (\n BaseSagaState, SagaRuntime,\n Call, Put, Select, Log, Stop, Complete,\n run_command_effect\n)\n\ndef add(x, y):\n return x + y\n\nclass State(BaseSagaState):\n math_result: int = 2\n command_result: str = \"\"\n\ndef my_saga():\n yield Log(\"info\", \"Starting saga\")\n initial_state = yield Select()\n math_result = yield Call(add, initial_state.math_result, 3)\n command_result = yield Call(run_command_effect, \"echo 'Hello World'\")\n if command_result is None:\n yield Log(\"error\", \"unable to run command\")\n yield Stop(\"hook failed, exited early\")\n yield Put({\"command_result\": command_result.stdout, \"math_result\": math_result})\n yield Complete(\"Saga completed successfully\")\n\nruntime = SagaRuntime(State())\nfinal_state = runtime.run(my_saga())\nprint(final_state.to_json())\n```\n\n## Building Claude Code Hooks\n\nClaude Saga handles input/output conventions of claude code hooks:\n\n```python\n#!/usr/bin/env python\nimport json\nimport sys\nfrom claude_saga import (\n BaseSagaState, SagaRuntime,\n validate_input_saga, parse_json_saga,\n Complete\n)\n\nclass HookState(BaseSagaState):\n # Add your custom state fields\n pass\n\ndef main_saga():\n # Validate and parse input\n # https://docs.anthropic.com/en/docs/claude-code/hooks#hook-input\n yield from validate_input_saga()\n # Adds input data to state\n yield from parse_json_saga()\n \n # Your hook logic here\n \n # Complete\n yield Complete(\"Hook executed successfully\")\n\ndef main():\n runtime = SagaRuntime(HookState())\n # Final state is an object that conforms to common json fields:\n # https://docs.anthropic.com/en/docs/claude-code/hooks#common-json-fields\n final_state = runtime.run(main_saga())\n # Claude Code exit code behavior:\n # https://docs.anthropic.com/en/docs/claude-code/hooks#simple%3A-exit-code\n print(json.dumps(final_state.to_json()))\n sys.exit(0 if final_state.continue_ else 1)\n\nif __name__ == \"__main__\":\n main()\n```\n\n\n## Effect Types\n\n### Call\nExecute functions, including(and especially) those with side-effects:\n```python\nresult = yield Call(function, arg1, arg2, kwarg=value)\n```\n\n### Put\nUpdate the state:\n```python\nyield Put({\"field\": \"value\"})\n# or with a function\nyield Put(lambda state: MyState(counter=state.counter + 1))\n```\n\n### Select\nRead from the state:\n```python\nstate = yield Select()\n# or with a selector\ncounter = yield Select(lambda state: state.counter)\n```\n\n### Log\nLog messages at different levels:\n```python\nyield Log(\"info\", \"Information message\")\nyield Log(\"error\", \"Error message\")\nyield Log(\"debug\", \"Debug message\") # Only shown with DEBUG=1\n```\n\n### Stop\nStop execution with an error, hook output contains `continue:false`:\n```python\nyield Stop(\"Error message\")\n```\n\n### Complete\nComplete saga successfully, hook output contains `continue:true`:\n```python\nyield Complete(\"Success message\")\n```\n\n## Common Effects\n\nThe library includes common effects \n- `log_info`, `log_error`, `log_debug`\n- `run_command_effect(cmd, cwd=None, capture_output=True)` - Run shell commands\n- `write_file_effect(path, content)` - Write files\n- `change_directory_effect(path)` - Change working directory\n- `create_directory_effect(path)` - Create directories\n- `connect_pycharm_debugger_effect()` - Connect to PyCharm debugger\n\nNotes:\n - 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. \n - If you want to terminate the saga on effect failure, check if the Call result is `None` and yield a `Stop`.\n\n## Common Sagas\n\nPre-built sagas for common tasks:\n\n- `validate_input_saga()` - Validate stdin input is provided\n- `parse_json_saga()` - Parse JSON from stdin into hook state (parses specifically for [Claude Code hook input](https://docs.anthropic.com/en/docs/claude-code/hooks#hook-input))\n\n# Development\n\n### Setup\n\n```bash\nuv pip install -e .\n```\n### Examples\n\nThe `examples/` directory contains a practical demonstration:\n\n- `simple_command_validator.py` - Claude Code hook for validating bash commands (saga version of the [official example](https://docs.anthropic.com/en/docs/claude-code/hooks#exit-code-example%3A-bash-command-validation))\n\n```bash\n# This will fail since the expected input to stdin is not provided\nuv run examples/simple_command_validator.py\n\n# Handle claude code stdin conventions, this command passes validation \necho '{\"tool_name\": \"Bash\", \"tool_input\": {\"command\": \"ls -la\"}}' | uv run examples/simple_command_validator.py\n\n# This command fails validation (uses grep instead of rg)\necho '{\"tool_name\": \"Bash\", \"tool_input\": {\"command\": \"grep pattern file.txt\"}}' | uv run examples/simple_command_validator.py\n\n```\n### Running Tests\n\n#### Unit Tests\nTest the core saga framework components:\n```bash\nuv run pytest tests/test_claude_saga.py -v\n```\n\n#### E2E Tests \nTest complete example hook behavior:\n```bash\nuv run pytest tests/test_e2e_simple_command_validator.py -v\n```\n\n#### All Tests\nRun the complete test suite:\n```bash\nuv run pytest tests/ -v\n```\n### Building\n\n```bash\nuv build\n```\n\n## License\n\nMIT License - see LICENSE file for details.\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\nI'd like to hear what common effects can be added to the core lib. e.g.\n- http_request_effect\n- mcp_request_effect\n\nFuture work must incorporate\n- parsing & validation for each hook's unique input/output behaviors, fields etc...\n- retry-able effects\n- cancel-able effects\n- parallel effects (e.g. `All`), see hypothetical async effects like `mcp_request` etc...\n- concurrent effects\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "A Redux Saga-like effect system for Python, designed for Claude Code hooks",
"version": "0.1.0",
"project_urls": {
"Homepage": "https://github.com/yourusername/claude-saga",
"Issues": "https://github.com/yourusername/claude-saga/issues",
"Repository": "https://github.com/yourusername/claude-saga"
},
"split_keywords": [
"claude",
" effects",
" functional",
" generator",
" hooks",
" saga",
" side-effects"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "dee6b45f182dbfcf91e7f7587744ba62bdc6a89a81b034456f467bd54d42c77d",
"md5": "732c05bf9b393a1bae59d9c6c184ab5a",
"sha256": "32cc46d9f8667c65975a7503419733e1895f1a11ee8897b33bb920cadb8b6114"
},
"downloads": -1,
"filename": "claude_saga-0.1.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "732c05bf9b393a1bae59d9c6c184ab5a",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.12",
"size": 7729,
"upload_time": "2025-08-30T14:04:38",
"upload_time_iso_8601": "2025-08-30T14:04:38.737654Z",
"url": "https://files.pythonhosted.org/packages/de/e6/b45f182dbfcf91e7f7587744ba62bdc6a89a81b034456f467bd54d42c77d/claude_saga-0.1.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "6e1e7db10ea88d646cd0e39a5fb8c502b323c0fc80302affae9d62dfd3fb69dd",
"md5": "e2859ee97d1aeb1ff056db5139715a92",
"sha256": "e4844ce0242d3a349beb0f50615874a7079fe87b3912fdb2f62f5b18202b820a"
},
"downloads": -1,
"filename": "claude_saga-0.1.0.tar.gz",
"has_sig": false,
"md5_digest": "e2859ee97d1aeb1ff056db5139715a92",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.12",
"size": 10935,
"upload_time": "2025-08-30T14:04:40",
"upload_time_iso_8601": "2025-08-30T14:04:40.073235Z",
"url": "https://files.pythonhosted.org/packages/6e/1e/7db10ea88d646cd0e39a5fb8c502b323c0fc80302affae9d62dfd3fb69dd/claude_saga-0.1.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-08-30 14:04:40",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "yourusername",
"github_project": "claude-saga",
"github_not_found": true,
"lcname": "claude-saga"
}