From 0f3e2f21fbadeb460dbed1e54b048b4d8018b9e3 Mon Sep 17 00:00:00 2001 From: Matt Fogel Date: Thu, 14 May 2026 09:40:25 -0400 Subject: [PATCH] Wire .provignore into capture and pre-push redaction --- .codex/config.toml | 2 ++ .codex/hooks.json | 50 +++++++++++++++++++++++++++ crates/prov-cli/src/commands/hook.rs | 28 ++++++++++++--- crates/prov-cli/tests/cli_pre_push.rs | 15 ++++++++ crates/prov-cli/tests/hook_capture.rs | 18 ++++++++++ 5 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 .codex/config.toml create mode 100644 .codex/hooks.json diff --git a/.codex/config.toml b/.codex/config.toml new file mode 100644 index 0000000..146af7e --- /dev/null +++ b/.codex/config.toml @@ -0,0 +1,2 @@ +[features] +hooks = true diff --git a/.codex/hooks.json b/.codex/hooks.json new file mode 100644 index 0000000..d2c584e --- /dev/null +++ b/.codex/hooks.json @@ -0,0 +1,50 @@ +{ + "hooks": { + "PostToolUse": [ + { + "hooks": [ + { + "command": "prov hook codex post-tool-use", + "timeout": 5, + "type": "command" + } + ], + "matcher": "Edit|Write" + } + ], + "SessionStart": [ + { + "hooks": [ + { + "command": "prov hook codex session-start", + "timeout": 5, + "type": "command" + } + ], + "matcher": "startup|resume|clear" + } + ], + "Stop": [ + { + "hooks": [ + { + "command": "prov hook codex stop", + "timeout": 5, + "type": "command" + } + ] + } + ], + "UserPromptSubmit": [ + { + "hooks": [ + { + "command": "prov hook codex user-prompt-submit", + "timeout": 5, + "type": "command" + } + ] + } + ] + } +} diff --git a/crates/prov-cli/src/commands/hook.rs b/crates/prov-cli/src/commands/hook.rs index 6ffd004..fffa23d 100644 --- a/crates/prov-cli/src/commands/hook.rs +++ b/crates/prov-cli/src/commands/hook.rs @@ -27,6 +27,7 @@ use serde::Deserialize; use prov_core::git::{Git, GitError}; use prov_core::privacy::is_prov_private; +use prov_core::redactor::provignore::{ProvIgnore, ProvIgnoreError}; use prov_core::redactor::Redactor; use prov_core::schema::{DerivedFrom, Edit, Note}; use prov_core::session::SessionId; @@ -129,7 +130,7 @@ pub fn run(args: Args) -> anyhow::Result<()> { let event_label = format!("{:?}", args.event); let result = match args.event { - Event::UserPromptSubmit => handle_user_prompt_submit(&staging, AgentHarness::Claude), + Event::UserPromptSubmit => handle_user_prompt_submit(&staging, AgentHarness::Claude, &git), Event::PostToolUse => { handle_post_tool_use(&staging, AgentHarness::Claude, Some(git.work_tree())) } @@ -158,7 +159,7 @@ fn handle_agent_event( git: &Git, ) -> Result<(), HandlerError> { match event { - AgentEvent::UserPromptSubmit => handle_user_prompt_submit(staging, harness), + AgentEvent::UserPromptSubmit => handle_user_prompt_submit(staging, harness, git), AgentEvent::PostToolUse => handle_post_tool_use(staging, harness, Some(git.work_tree())), AgentEvent::Stop => handle_stop(staging, harness), AgentEvent::SessionStart => handle_session_start(staging, harness), @@ -210,6 +211,7 @@ struct UserPromptSubmitPayload { fn handle_user_prompt_submit( staging: &Staging, _harness: AgentHarness, + git: &Git, ) -> Result<(), HandlerError> { let payload: UserPromptSubmitPayload = read_stdin_json()?; let raw_session = payload @@ -229,7 +231,13 @@ fn handle_user_prompt_submit( // Redact even staged content. The redactor is the primary defense; pre-push // (U8) is the second line. - let redactor = Redactor::new(); + let redactor = match redactor_for_repo(git.work_tree()) { + Ok(redactor) => redactor, + Err(e) => { + let _ = staging.append_log(&format!("{}: .provignore load failed: {e}", now_iso8601())); + Redactor::new() + } + }; let redacted = redactor.redact(&prompt); let turn_index = staging.count_turns(&sid, private)?; @@ -1476,7 +1484,14 @@ fn handle_pre_push(git: &Git) -> Result { } let mut blocks: Vec = Vec::new(); - let redactor = Redactor::new(); + let redactor = match redactor_for_repo(git.work_tree()) { + Ok(redactor) => redactor, + Err(e) => { + return Ok(PrePushOutcome::Block(vec![format!( + "prov pre-push: .provignore could not be loaded: {e}" + )])); + } + }; for raw in buf.lines() { let mut parts = raw.split_whitespace(); @@ -1619,6 +1634,11 @@ fn is_full_hex_sha(s: &str) -> bool { // shared helpers // ================================================================= +fn redactor_for_repo(work_tree: &Path) -> Result { + let provignore = ProvIgnore::from_path(work_tree.join(".provignore"))?; + Ok(Redactor::new().with_provignore(provignore)) +} + #[derive(Debug, thiserror::Error)] enum HandlerError { #[error(transparent)] diff --git a/crates/prov-cli/tests/cli_pre_push.rs b/crates/prov-cli/tests/cli_pre_push.rs index 42f8bcf..546b21b 100644 --- a/crates/prov-cli/tests/cli_pre_push.rs +++ b/crates/prov-cli/tests/cli_pre_push.rs @@ -165,6 +165,21 @@ fn pre_push_blocks_unredacted_aws_key_in_new_note() { .stderr(predicate::str::contains(&head)); } +#[test] +fn pre_push_blocks_repo_provignore_pattern_in_new_note() { + let (tmp, head, public_sha) = repo_with_public_note("Discuss Project Phoenix rollout"); + std::fs::write(tmp.path().join(".provignore"), "Project Phoenix\n").unwrap(); + let stdin = format!("{NOTES_REF_PUBLIC} {public_sha} {NOTES_REF_PUBLIC} {ZERO_SHA}\n"); + + prov_in(tmp.path()) + .args(["hook", "pre-push"]) + .write_stdin(stdin) + .assert() + .failure() + .stderr(predicate::str::contains("provignore-rule:0")) + .stderr(predicate::str::contains(&head)); +} + #[test] fn pre_push_blocks_when_local_ref_is_private_regardless_of_remote() { // Catches the manual-mapping bypass: diff --git a/crates/prov-cli/tests/hook_capture.rs b/crates/prov-cli/tests/hook_capture.rs index 9b7e94b..6cacb1d 100644 --- a/crates/prov-cli/tests/hook_capture.rs +++ b/crates/prov-cli/tests/hook_capture.rs @@ -106,6 +106,24 @@ fn user_prompt_submit_creates_turn_file() { assert!(body.contains("hello function")); } +#[test] +fn user_prompt_submit_redacts_repo_provignore_patterns() { + let tmp = init_repo(); + std::fs::write(tmp.path().join(".provignore"), "Project Phoenix\n").unwrap(); + let payload = serde_json::json!({ + "session_id": SID, + "prompt": "wire up Project Phoenix launch notes", + "cwd": tmp.path(), + }); + + fire_hook(tmp.path(), "user-prompt-submit", &payload.to_string()); + + let turn_path = staging_path(tmp.path()).join(SID).join("turn-0.json"); + let body = std::fs::read_to_string(turn_path).unwrap(); + assert!(body.contains("[REDACTED:provignore-rule:0]")); + assert!(!body.contains("Project Phoenix")); +} + #[test] fn adapter_qualified_claude_prompt_matches_legacy_prompt_capture() { let tmp = init_repo();