Plugin development
Extend validation with exec plugins
Overview
Plugins let you add custom validation logic to klaudiush. They're standalone executables that communicate via JSON over stdin/stdout. Bash scripts, Python, Go binaries - anything that can read stdin and write JSON works.
Each invocation is a fresh process. Plugins are stateless by default and changes to the script take effect immediately without a restart.
Quick start
1. Write a plugin
#!/usr/bin/env bash
set -euo pipefail
# Handle --info flag (metadata request)
if [[ "${1:-}" == "--info" ]]; then
echo '{"name":"my-plugin","version":"1.0.0","description":"My custom validator"}'
exit 0
fi
# Read validation request from stdin
read -r request
tool_name=$(echo "$request" | jq -r '.tool_name')
command=$(echo "$request" | jq -r '.command // empty')
# Validation logic
if [[ "$tool_name" == "Bash" ]] && [[ "$command" == *"sudo"* ]]; then
cat <<EOF
{"passed":false,"should_block":true,"message":"sudo commands are not allowed","error_code":"NO_SUDO"}
EOF
exit 0
fi
echo '{"passed":true,"should_block":false}' 2. Install and configure
chmod +x my-plugin.sh
mkdir -p "${XDG_DATA_HOME:-$HOME/.local/share}"/klaudiush/plugins
cp my-plugin.sh "${XDG_DATA_HOME:-$HOME/.local/share}"/klaudiush/plugins/ [plugins]
enabled = true
[[plugins.plugins]]
name = "my-plugin"
type = "exec"
path = "~/.local/share/klaudiush/plugins/my-plugin.sh"
[plugins.plugins.predicate]
event_types = ["PreToolUse"]
tool_types = ["Bash"] 3. Test it
./my-plugin.sh --info
echo '{"tool_name":"Bash","command":"sudo rm -rf /"}' | ./my-plugin.sh
echo '{"tool_name":"Bash","command":"ls"}' | ./my-plugin.sh Protocol reference
klaudiush calls your plugin in two ways: --info for metadata, and stdin JSON
for validation requests.
Info request
Called with --info to get plugin metadata. Return JSON with name and version fields.
Validate request
klaudiush writes a JSON object to stdin. Fields present depend on the tool:
| Field | Present when | Description |
|---|---|---|
event_type | Always | PreToolUse, PostToolUse |
tool_name | Always | Bash, Write, Edit, etc. |
command | Bash tool | Shell command being run |
file_path | Write/Edit/Read | Path to the file |
content | Write tool | Content being written |
config | If configured | Plugin-specific config from TOML |
Validate response
Return a JSON object with your validation result:
| Field | Required | Description |
|---|---|---|
passed | Yes | Whether validation passed |
should_block | Yes | Whether to block the operation |
message | No | Human-readable result message |
error_code | No | Unique error identifier |
fix_hint | No | Short fix suggestion |
Always exit 0
Communicate validation failures through JSON, not exit codes. Non-zero exits cause fail-open behavior - the operation proceeds and the error is logged.
Predicate matching
Predicates control when your plugin is invoked. All conditions must match (AND logic). Omitting a predicate means "match all" for that dimension.
# Git commits only
[plugins.plugins.predicate]
event_types = ["PreToolUse"]
tool_types = ["Bash"]
command_patterns = ["^git commit"]
# Go file writes
[plugins.plugins.predicate]
event_types = ["PreToolUse"]
tool_types = ["Write", "Edit"]
file_patterns = ["**/*.go"]
# Catch-all (matches everything)
[plugins.plugins.predicate] Python example
Same protocol, different language. This plugin blocks binary file writes:
#!/usr/bin/env python3
import sys, json
def main():
if len(sys.argv) > 1 and sys.argv[1] == "--info":
print(json.dumps({"name": "python-validator", "version": "1.0.0",
"description": "Example Python validator"}))
return
request = json.load(sys.stdin)
tool_name = request.get("tool_name", "")
file_path = request.get("file_path", "")
if tool_name in ("Write", "Edit") and file_path.endswith(".exe"):
print(json.dumps({"passed": False, "should_block": True,
"message": "Binary files (.exe) are not allowed",
"error_code": "NO_BINARIES"}))
return
print(json.dumps({"passed": True, "should_block": False}))
if __name__ == "__main__":
main() Best practices
- Return early for non-matching contexts instead of doing unnecessary work.
- Use narrow predicates so the plugin is only spawned when relevant.
- Keep startup fast - each invocation is a fresh process.
- Set reasonable timeouts: 1-5s for fast checks, 10-30s for external APIs.
- Only write the JSON response to stdout. Diagnostics go to stderr.
- Use
error_codefor programmatic handling and doc links.