codex_wrapper/dangerous.rs
1//! Opt-in access to the flags that disable codex's safety controls.
2//!
3//! `--dangerously-bypass-approvals-and-sandbox` turns off every approval
4//! prompt and the sandbox. `--dangerously-bypass-hook-trust` lets configured
5//! hooks run without confirmation. Both were plain builder methods, reachable
6//! from any chain by autocomplete, by a copied snippet, or by an agent editing
7//! a call site. A method name is not a barrier.
8//!
9//! They now need two things that cannot both happen by accident:
10//!
11//! 1. A [`DangerousClient`], which only constructs when
12//! `CODEX_WRAPPER_ALLOW_DANGEROUS` is set in the process environment.
13//! 2. A call through [`Dangerous`], passing that client, which re-checks the
14//! variable at the point of use.
15//!
16//! The second check is not redundant. A client built while the variable was
17//! set stops working the moment it is unset, so the gate reflects the
18//! environment at the moment the bypass is applied rather than whenever the
19//! client happened to be created.
20//!
21//! The name matches the sibling crate's `CLAUDE_WRAPPER_ALLOW_DANGEROUS`.
22//!
23//! # Example
24//!
25//! ```
26//! use codex_wrapper::{ExecCommand, dangerous::{Dangerous, DangerousClient}};
27//!
28//! // Without the environment variable, there is no way through.
29//! assert!(DangerousClient::new().is_err());
30//! ```
31//!
32//! ```no_run
33//! use codex_wrapper::{CodexCommand, ExecCommand};
34//! use codex_wrapper::dangerous::{Dangerous, DangerousClient};
35//!
36//! # async fn example(codex: &codex_wrapper::Codex) -> codex_wrapper::Result<()> {
37//! // With CODEX_WRAPPER_ALLOW_DANGEROUS set in the environment:
38//! let allow = DangerousClient::new()?;
39//! let output = ExecCommand::new("rewrite everything")
40//! .bypass_approvals_and_sandbox(&allow)?
41//! .execute(codex)
42//! .await?;
43//! # let _ = output;
44//! # Ok(())
45//! # }
46//! ```
47
48use crate::error::{Error, Result};
49
50/// The environment variable that unlocks the bypass flags.
51pub const ALLOW_DANGEROUS_ENV: &str = "CODEX_WRAPPER_ALLOW_DANGEROUS";
52
53/// Proof that bypassing codex's safety controls is permitted here.
54///
55/// Constructing one requires [`ALLOW_DANGEROUS_ENV`] to be set. Holding one is
56/// not enough on its own: every [`Dangerous`] method re-checks.
57#[derive(Debug, Clone, Copy)]
58pub struct DangerousClient {
59 // Keeps the type unconstructible except through `new`.
60 _private: (),
61}
62
63impl DangerousClient {
64 /// `Err(Error::DangerousNotAllowed)` unless [`ALLOW_DANGEROUS_ENV`] is set
65 /// to a non-empty value.
66 pub fn new() -> Result<Self> {
67 allowed(&|key| std::env::var(key).ok())?;
68 Ok(Self { _private: () })
69 }
70
71 /// A client without the environment check, for testing the second gate.
72 ///
73 /// Exists so a test can prove that holding a client is not sufficient,
74 /// without setting a process-wide variable that would race other tests.
75 #[cfg(test)]
76 pub(crate) fn unchecked() -> Self {
77 Self { _private: () }
78 }
79}
80
81/// The gate itself, over an injected environment lookup.
82///
83/// Injected rather than reading the process environment directly so tests can
84/// exercise the allowed path without mutating global state, which would make
85/// them race each other.
86pub(crate) fn allowed(env: &impl Fn(&str) -> Option<String>) -> Result<()> {
87 match env(ALLOW_DANGEROUS_ENV) {
88 Some(value) if !value.trim().is_empty() => Ok(()),
89 _ => Err(Error::DangerousNotAllowed {
90 variable: ALLOW_DANGEROUS_ENV,
91 }),
92 }
93}
94
95mod sealed {
96 pub trait Sealed {}
97}
98
99/// Bypassing codex's safety controls, for the builders that support it.
100///
101/// Sealed: this exists to gate an existing capability, not to be implemented
102/// elsewhere.
103pub trait Dangerous: sealed::Sealed + Sized {
104 /// Disable every approval prompt and the sandbox
105 /// (`--dangerously-bypass-approvals-and-sandbox`).
106 ///
107 /// The model's shell commands run with the permissions of the calling
108 /// process, against the real filesystem, with nothing to contain a
109 /// mistake.
110 ///
111 /// # Errors
112 ///
113 /// [`Error::DangerousNotAllowed`] if [`ALLOW_DANGEROUS_ENV`] is not set
114 /// at the moment of the call.
115 fn bypass_approvals_and_sandbox(self, allow: &DangerousClient) -> Result<Self>;
116
117 /// Let configured hooks run without the trust prompt
118 /// (`--dangerously-bypass-hook-trust`).
119 ///
120 /// # Errors
121 ///
122 /// [`Error::DangerousNotAllowed`] if [`ALLOW_DANGEROUS_ENV`] is not set
123 /// at the moment of the call.
124 fn bypass_hook_trust(self, allow: &DangerousClient) -> Result<Self>;
125}
126
127/// Implement [`Dangerous`] over the crate-internal setters.
128///
129/// The setters are `pub(crate)`, so this trait is the only way to reach them
130/// from outside, and every path through it is gated.
131macro_rules! impl_dangerous {
132 ($($ty:ty),+ $(,)?) => {
133 $(
134 impl sealed::Sealed for $ty {}
135
136 impl Dangerous for $ty {
137 fn bypass_approvals_and_sandbox(
138 self,
139 _allow: &DangerousClient,
140 ) -> Result<Self> {
141 allowed(&|key| std::env::var(key).ok())?;
142 Ok(self.set_bypass_approvals_and_sandbox())
143 }
144
145 fn bypass_hook_trust(self, _allow: &DangerousClient) -> Result<Self> {
146 allowed(&|key| std::env::var(key).ok())?;
147 Ok(self.set_bypass_hook_trust())
148 }
149 }
150 )+
151 };
152}
153
154impl_dangerous!(
155 crate::command::exec::ExecCommand,
156 crate::command::exec::ExecResumeCommand,
157 crate::command::review::ReviewCommand,
158 crate::command::fork::ForkCommand,
159 crate::command::resume::ResumeCommand,
160);
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn the_gate_is_closed_by_default() {
168 // Not set in this process, and nothing here sets it.
169 let err = DangerousClient::new().unwrap_err();
170 assert!(matches!(err, Error::DangerousNotAllowed { .. }), "{err:?}");
171 assert!(err.to_string().contains(ALLOW_DANGEROUS_ENV));
172 }
173
174 #[test]
175 fn the_gate_opens_for_a_non_empty_value() {
176 assert!(allowed(&|_| Some("1".into())).is_ok());
177 assert!(allowed(&|_| Some("anything".into())).is_ok());
178 }
179
180 /// An exported-but-empty variable is the shape a shell leaves behind after
181 /// `export X=`, and it must not count as permission.
182 #[test]
183 fn a_blank_value_does_not_open_the_gate() {
184 assert!(allowed(&|_| Some(String::new())).is_err());
185 assert!(allowed(&|_| Some(" ".into())).is_err());
186 assert!(allowed(&|_| None).is_err());
187 }
188
189 /// The second gate, and the reason it is not redundant: a client built
190 /// while the variable was set must stop working once it is gone.
191 #[test]
192 fn holding_a_client_is_not_enough() {
193 use crate::command::exec::ExecCommand;
194
195 let stale = DangerousClient::unchecked();
196 let err = ExecCommand::new("rewrite everything")
197 .bypass_approvals_and_sandbox(&stale)
198 .unwrap_err();
199 assert!(matches!(err, Error::DangerousNotAllowed { .. }), "{err:?}");
200
201 let err = ExecCommand::new("rewrite everything")
202 .bypass_hook_trust(&stale)
203 .unwrap_err();
204 assert!(matches!(err, Error::DangerousNotAllowed { .. }), "{err:?}");
205 }
206
207 /// Gating must not have disconnected the flags from the command line.
208 #[test]
209 fn the_flags_still_reach_argv_once_set() {
210 use crate::command::exec::ExecCommand;
211 use crate::command::{CodexCommand, review::ReviewCommand};
212
213 let args = ExecCommand::new("x")
214 .set_bypass_approvals_and_sandbox()
215 .set_bypass_hook_trust()
216 .args();
217 assert!(
218 args.iter()
219 .any(|a| a == "--dangerously-bypass-approvals-and-sandbox"),
220 "{args:?}"
221 );
222 assert!(
223 args.iter().any(|a| a == "--dangerously-bypass-hook-trust"),
224 "{args:?}"
225 );
226
227 let args = ReviewCommand::new()
228 .uncommitted()
229 .set_bypass_approvals_and_sandbox()
230 .args();
231 assert!(
232 args.iter()
233 .any(|a| a == "--dangerously-bypass-approvals-and-sandbox"),
234 "{args:?}"
235 );
236 }
237}