mirror of
https://github.com/openai/codex.git
synced 2026-09-14 11:57:03 +00:00
- this allows blocking the user's prompts from executing, and also
prevents them from entering history
- handles the edge case where you can both prevent the user's prompt AND
add n amount of additionalContexts
- refactors some old code into common.rs where hooks overlap
functionality
- refactors additionalContext being previously added to user messages,
instead we use developer messages for them
- handles queued messages correctly
Sample hook for testing - if you write "[block-user-submit]" this hook
will stop the thread:
example run
```
› sup
• Running UserPromptSubmit hook: reading the observatory notes
UserPromptSubmit hook (completed)
warning: wizard-tower UserPromptSubmit demo inspected: sup
hook context: Wizard Tower UserPromptSubmit demo fired. For this reply only, include the exact
phrase 'observatory lanterns lit' exactly once near the end.
• Just riding the cosmic wave and ready to help, my friend. What are we building today? observatory
lanterns lit
› and [block-user-submit]
• Running UserPromptSubmit hook: reading the observatory notes
UserPromptSubmit hook (stopped)
warning: wizard-tower UserPromptSubmit demo blocked the prompt on purpose.
stop: Wizard Tower demo block: remove [block-user-submit] to continue.
```
.codex/config.toml
```
[features]
codex_hooks = true
```
.codex/hooks.json
```
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "/usr/bin/python3 .codex/hooks/user_prompt_submit_demo.py",
"timeoutSec": 10,
"statusMessage": "reading the observatory notes"
}
]
}
]
}
}
```
.codex/hooks/user_prompt_submit_demo.py
```
#!/usr/bin/env python3
import json
import sys
from pathlib import Path
def prompt_from_payload(payload: dict) -> str:
prompt = payload.get("prompt")
if isinstance(prompt, str) and prompt.strip():
return prompt.strip()
event = payload.get("event")
if isinstance(event, dict):
user_prompt = event.get("user_prompt")
if isinstance(user_prompt, str):
return user_prompt.strip()
return ""
def main() -> int:
payload = json.load(sys.stdin)
prompt = prompt_from_payload(payload)
cwd = Path(payload.get("cwd", ".")).name or "wizard-tower"
if "[block-user-submit]" in prompt:
print(
json.dumps(
{
"systemMessage": (
f"{cwd} UserPromptSubmit demo blocked the prompt on purpose."
),
"decision": "block",
"reason": (
"Wizard Tower demo block: remove [block-user-submit] to continue."
),
}
)
)
return 0
prompt_preview = prompt or "(empty prompt)"
if len(prompt_preview) > 80:
prompt_preview = f"{prompt_preview[:77]}..."
print(
json.dumps(
{
"systemMessage": (
f"{cwd} UserPromptSubmit demo inspected: {prompt_preview}"
),
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": (
"Wizard Tower UserPromptSubmit demo fired. "
"For this reply only, include the exact phrase "
"'observatory lanterns lit' exactly once near the end."
),
},
}
)
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
```
138 lines
3.9 KiB
Rust
138 lines
3.9 KiB
Rust
use codex_config::ConfigLayerStack;
|
|
use tokio::process::Command;
|
|
|
|
use crate::engine::ClaudeHooksEngine;
|
|
use crate::engine::CommandShell;
|
|
use crate::events::session_start::SessionStartOutcome;
|
|
use crate::events::session_start::SessionStartRequest;
|
|
use crate::events::stop::StopOutcome;
|
|
use crate::events::stop::StopRequest;
|
|
use crate::events::user_prompt_submit::UserPromptSubmitOutcome;
|
|
use crate::events::user_prompt_submit::UserPromptSubmitRequest;
|
|
use crate::types::Hook;
|
|
use crate::types::HookEvent;
|
|
use crate::types::HookPayload;
|
|
use crate::types::HookResponse;
|
|
|
|
#[derive(Default, Clone)]
|
|
pub struct HooksConfig {
|
|
pub legacy_notify_argv: Option<Vec<String>>,
|
|
pub feature_enabled: bool,
|
|
pub config_layer_stack: Option<ConfigLayerStack>,
|
|
pub shell_program: Option<String>,
|
|
pub shell_args: Vec<String>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct Hooks {
|
|
after_agent: Vec<Hook>,
|
|
after_tool_use: Vec<Hook>,
|
|
engine: ClaudeHooksEngine,
|
|
}
|
|
|
|
impl Default for Hooks {
|
|
fn default() -> Self {
|
|
Self::new(HooksConfig::default())
|
|
}
|
|
}
|
|
|
|
impl Hooks {
|
|
pub fn new(config: HooksConfig) -> Self {
|
|
let after_agent = config
|
|
.legacy_notify_argv
|
|
.filter(|argv| !argv.is_empty() && !argv[0].is_empty())
|
|
.map(crate::notify_hook)
|
|
.into_iter()
|
|
.collect();
|
|
let engine = ClaudeHooksEngine::new(
|
|
config.feature_enabled,
|
|
config.config_layer_stack.as_ref(),
|
|
CommandShell {
|
|
program: config.shell_program.unwrap_or_default(),
|
|
args: config.shell_args,
|
|
},
|
|
);
|
|
Self {
|
|
after_agent,
|
|
after_tool_use: Vec::new(),
|
|
engine,
|
|
}
|
|
}
|
|
|
|
pub fn startup_warnings(&self) -> &[String] {
|
|
self.engine.warnings()
|
|
}
|
|
|
|
fn hooks_for_event(&self, hook_event: &HookEvent) -> &[Hook] {
|
|
match hook_event {
|
|
HookEvent::AfterAgent { .. } => &self.after_agent,
|
|
HookEvent::AfterToolUse { .. } => &self.after_tool_use,
|
|
}
|
|
}
|
|
|
|
pub async fn dispatch(&self, hook_payload: HookPayload) -> Vec<HookResponse> {
|
|
let hooks = self.hooks_for_event(&hook_payload.hook_event);
|
|
let mut outcomes = Vec::with_capacity(hooks.len());
|
|
for hook in hooks {
|
|
let outcome = hook.execute(&hook_payload).await;
|
|
let should_abort_operation = outcome.result.should_abort_operation();
|
|
outcomes.push(outcome);
|
|
if should_abort_operation {
|
|
break;
|
|
}
|
|
}
|
|
|
|
outcomes
|
|
}
|
|
|
|
pub fn preview_session_start(
|
|
&self,
|
|
request: &SessionStartRequest,
|
|
) -> Vec<codex_protocol::protocol::HookRunSummary> {
|
|
self.engine.preview_session_start(request)
|
|
}
|
|
|
|
pub async fn run_session_start(
|
|
&self,
|
|
request: SessionStartRequest,
|
|
turn_id: Option<String>,
|
|
) -> SessionStartOutcome {
|
|
self.engine.run_session_start(request, turn_id).await
|
|
}
|
|
|
|
pub fn preview_user_prompt_submit(
|
|
&self,
|
|
request: &UserPromptSubmitRequest,
|
|
) -> Vec<codex_protocol::protocol::HookRunSummary> {
|
|
self.engine.preview_user_prompt_submit(request)
|
|
}
|
|
|
|
pub async fn run_user_prompt_submit(
|
|
&self,
|
|
request: UserPromptSubmitRequest,
|
|
) -> UserPromptSubmitOutcome {
|
|
self.engine.run_user_prompt_submit(request).await
|
|
}
|
|
|
|
pub fn preview_stop(
|
|
&self,
|
|
request: &StopRequest,
|
|
) -> Vec<codex_protocol::protocol::HookRunSummary> {
|
|
self.engine.preview_stop(request)
|
|
}
|
|
|
|
pub async fn run_stop(&self, request: StopRequest) -> StopOutcome {
|
|
self.engine.run_stop(request).await
|
|
}
|
|
}
|
|
|
|
pub fn command_from_argv(argv: &[String]) -> Option<Command> {
|
|
let (program, args) = argv.split_first()?;
|
|
if program.is_empty() {
|
|
return None;
|
|
}
|
|
let mut command = Command::new(program);
|
|
command.args(args);
|
|
Some(command)
|
|
}
|