Skip to content

Commit 59ea9a0

Browse files
committed
Add request hook system
1 parent 41a1853 commit 59ea9a0

19 files changed

Lines changed: 814 additions & 15 deletions

AGENTS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ API response → KeyClaw Proxy → scan for {{KEYCLAW_SECRET_<prefix>_<16 hex ch
4747
| Module | Purpose | Key Types |
4848
|--------|---------|-----------|
4949
| `gitleaks_rules.rs` | Bundled gitleaks rule loading + compiled regex matching | `RuleSet` |
50+
| `hooks.rs` | Configured request-side hook parsing and execution | `HookRunner` |
5051
| `pipeline.rs` | Orchestrates rewrite + policy evaluation | `Processor`, `RewriteResult` |
5152
| `placeholder.rs` | Placeholder parsing, generation, and resolution | `make_id()`, `resolve_placeholders()` |
5253
| `redaction.rs` | JSON tree walker, notice injection | `walk_json_strings()`, `inject_redaction_notice()` |
@@ -87,6 +88,10 @@ Edit `src/launcher.rs` to extend the clap surface and subcommand dispatch, then
8788

8889
Edit `src/redaction.rs``inject_redaction_notice_with_mode()` and `src/config.rs` for `KEYCLAW_NOTICE_MODE`. The notice is injected differently for Anthropic (appended to `system` field) vs OpenAI (added as `developer` role message), and the shipped modes are `verbose`, `minimal`, and `off`.
8990

91+
### Adding or changing hooks
92+
93+
Edit `src/hooks.rs` for hook parsing/execution and `src/config.rs` for `[[hooks]]` config loading. Request-side hook dispatch is wired through `src/pipeline.rs`, with call sites in `src/proxy/http.rs`, `src/proxy/websocket.rs`, and `src/launcher.rs`.
94+
9095
## Important Patterns
9196

9297
### Placeholder format
@@ -125,6 +130,7 @@ All errors use `KeyclawError` with optional deterministic codes:
125130
- `invalid_json` — JSON parse/rewrite failed
126131
- `request_timeout` — request body read timed out before inspection completed
127132
- `strict_resolve_failed` — placeholder resolution failed in strict mode
133+
- `hook_blocked` — a configured hook rejected the request
128134

129135
Check errors with `code_of(&err)` to get the code string.
130136

CLAUDE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ API response → KeyClaw Proxy → scan for {{KEYCLAW_SECRET_<prefix>_<16 hex ch
4747
| Module | Purpose | Key Types |
4848
|--------|---------|-----------|
4949
| `gitleaks_rules.rs` | Bundled gitleaks rule loading + compiled regex matching | `RuleSet` |
50+
| `hooks.rs` | Configured request-side hook parsing and execution | `HookRunner` |
5051
| `pipeline.rs` | Orchestrates rewrite + policy evaluation | `Processor`, `RewriteResult` |
5152
| `placeholder.rs` | Placeholder parsing, generation, and resolution | `make_id()`, `resolve_placeholders()` |
5253
| `redaction.rs` | JSON tree walker, notice injection | `walk_json_strings()`, `inject_redaction_notice()` |
@@ -87,6 +88,10 @@ Edit `src/launcher.rs` to extend the clap surface and subcommand dispatch, then
8788

8889
Edit `src/redaction.rs``inject_redaction_notice_with_mode()` and `src/config.rs` for `KEYCLAW_NOTICE_MODE`. The notice is injected differently for Anthropic (appended to `system` field) vs OpenAI (added as `developer` role message), and the shipped modes are `verbose`, `minimal`, and `off`.
8990

91+
### Adding or changing hooks
92+
93+
Edit `src/hooks.rs` for hook parsing/execution and `src/config.rs` for `[[hooks]]` config loading. Request-side hook dispatch is wired through `src/pipeline.rs`, with call sites in `src/proxy/http.rs`, `src/proxy/websocket.rs`, and `src/launcher.rs`.
94+
9095
## Important Patterns
9196

9297
### Placeholder format
@@ -125,6 +130,7 @@ All errors use `KeyclawError` with optional deterministic codes:
125130
- `invalid_json` — JSON parse/rewrite failed
126131
- `request_timeout` — request body read timed out before inspection completed
127132
- `strict_resolve_failed` — placeholder resolution failed in strict mode
133+
- `hook_blocked` — a configured hook rejected the request
128134

129135
Check errors with `code_of(&err)` to get the code string.
130136

README.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,9 +366,27 @@ include = ["*my-custom-api.com*"]
366366
rule_ids = ["generic-api-key"]
367367
patterns = ["^sk-test-"]
368368
secret_sha256 = ["0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"]
369+
370+
[[hooks]]
371+
event = "secret_detected"
372+
rule_ids = ["aws-access-key", "generic-api-key"]
373+
action = "exec"
374+
command = "notify-slack --channel security"
375+
376+
[[hooks]]
377+
event = "request_redacted"
378+
rule_ids = ["*"]
379+
action = "log"
380+
path = "~/.keyclaw/hooks.log"
381+
382+
[[hooks]]
383+
event = "secret_detected"
384+
rule_ids = ["generic-api-key"]
385+
action = "block"
386+
message = "production key detected"
369387
```
370388

371-
Supported file sections today are `proxy`, `vault`, `logging`, `notice`, `detection`, `audit`, `hosts`, and `allowlist`. Use the file for steady-state local settings, then reach for env vars when you want a one-off override.
389+
Supported file sections today are `proxy`, `vault`, `logging`, `notice`, `detection`, `audit`, `hosts`, `allowlist`, and `hooks`. Use the file for steady-state local settings, then reach for env vars when you want a one-off override.
372390

373391
Allowlist entries let you intentionally skip redaction for known-safe matches:
374392

@@ -389,6 +407,16 @@ Disable persistent audit logging with:
389407
path = "off"
390408
```
391409

410+
Hook entries let you trigger local side effects from request-side events without exposing the raw secret:
411+
412+
- `event = "secret_detected"` fires when a secret match is found during request rewriting
413+
- `event = "request_redacted"` fires after a request has been rewritten, just before it is forwarded upstream
414+
- `action = "exec"` runs a local command with sanitized metadata in env vars and a JSON payload on `stdin`
415+
- `action = "log"` appends a JSON line to the configured file
416+
- `action = "block"` rejects matching `secret_detected` requests with `hook_blocked`
417+
418+
Hook payloads include only `event`, `rule_id`, `placeholder`, and `request_host`. Raw secret values are never passed to hook commands or hook log files.
419+
392420
### Environment Variables
393421

394422
| Variable | Default | Description |
@@ -488,6 +516,7 @@ KeyClaw uses deterministic error codes for programmatic handling:
488516
| `invalid_json` | Failed to parse or rewrite request JSON |
489517
| `request_timeout` | Request body read timed out before inspection completed |
490518
| `strict_resolve_failed` | Placeholder resolution failed in strict mode |
519+
| `hook_blocked` | A configured hook rejected the request |
491520

492521
## Security Model
493522

@@ -522,6 +551,7 @@ src/
522551
├── config.rs # Env + TOML configuration
523552
├── entropy.rs # High-entropy token detection
524553
├── gitleaks_rules.rs # Bundled gitleaks rule loading + native regex compilation
554+
├── hooks.rs # Configured request-side hook execution
525555
├── pipeline.rs # Request rewrite pipeline
526556
├── placeholder.rs # Placeholder parsing, generation, and resolution
527557
├── redaction.rs # JSON walker + notice injection

src/config.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use std::time::Duration;
1212
use serde::Deserialize;
1313

1414
use crate::allowlist::Allowlist;
15+
use crate::hooks::{Hook, RawHookConfig};
1516
use crate::logging::LogLevel;
1617
use crate::redaction::NoticeMode;
1718

@@ -63,6 +64,8 @@ pub struct Config {
6364
pub audit_log_path: Option<PathBuf>,
6465
/// Operator-defined allowlist entries.
6566
pub allowlist: Allowlist,
67+
/// Configured request-side hook actions.
68+
pub hooks: Vec<Hook>,
6669
pub(crate) config_file_status: ConfigFileStatus,
6770
}
6871

@@ -84,6 +87,7 @@ struct FileConfig {
8487
audit: FileAuditConfig,
8588
hosts: FileHostsConfig,
8689
allowlist: FileAllowlistConfig,
90+
hooks: Vec<RawHookConfig>,
8791
}
8892

8993
#[derive(Debug, Default, Deserialize)]
@@ -229,6 +233,7 @@ impl Config {
229233
entropy_min_len: 20,
230234
audit_log_path: Some(crate::audit::default_audit_log_path()),
231235
allowlist: Allowlist::default(),
236+
hooks: Vec::new(),
232237
config_file_status: ConfigFileStatus::Missing(default_config_path()),
233238
}
234239
}
@@ -351,6 +356,8 @@ impl Config {
351356
&file_cfg.allowlist.secret_sha256,
352357
)
353358
.map_err(|err| format!("config file {} has invalid {err}", path.display()))?;
359+
self.hooks = crate::hooks::parse_hooks(&file_cfg.hooks)
360+
.map_err(|err| format!("config file {} has invalid {err}", path.display()))?;
354361

355362
Ok(())
356363
}

src/errors.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ pub const CODE_INVALID_JSON: &str = "invalid_json";
1414
pub const CODE_REQUEST_TIMEOUT: &str = "request_timeout";
1515
/// Error code returned when strict placeholder resolution cannot complete.
1616
pub const CODE_STRICT_RESOLVE_FAILED: &str = "strict_resolve_failed";
17+
/// Error code returned when a configured hook blocks request processing.
18+
pub const CODE_HOOK_BLOCKED: &str = "hook_blocked";
1719

1820
#[derive(Debug, Clone)]
1921
pub struct KeyclawError {

0 commit comments

Comments
 (0)