Skip to main content

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}