Skip to content

Shell快照与命令环境

从 Session 创建 ShellSnapshot,追踪它如何绑定到 TurnEnvironment、包装本地命令、恢复环境覆盖并在退出后清理。

基于rust-v0.150.0
CodexRustRuntimeShell

Shell快照与命令环境 ​

在 Codex 里执行 echo 并不只是把一段字符串交给 shell。命令可能需要用户启动 shell 时已有的函数、alias、 shell option 和导出的环境变量;同时 Codex 还必须重新放入 thread id、权限 profile、受管代理和运行时 PATH。 这两个来源如果没有明确的先后关系,就会出现“用户 shell 能找到命令,但 Codex 找不到”,或者“快照恢复了旧权限 环境”的问题。

本文面向已经读过 Session启动预热、Session启动上下文、 UserShellTask流程 和 Codex信任边界 的读者。 本文只研究本地 shell snapshot 的创建、绑定、命令包装和清理,不把它当成环境沙箱,也不展开具体平台 sandbox 实现。读完后,读者应能从 Session::spawn_internal 追到一次 shell_command 或 exec_command 的最终命令, 解释快照何时不可用、哪些变量必须在 source 后恢复,以及文件为什么会在 Session 结束后删除。

1. 入口 ​

Shell snapshot 的入口不是工具 handler,而是 Session 装配。ShellSnapshot 保存 thread 级配置,真正的 ShellSnapshotFile 则按 environment 的 cwd 异步生成。ShellSnapshotV2 还为支持该 capability 的 executor 提供请求式快照,不在 Codex host 上落盘。

源码位置:codex-rs/features/src/lib.rs :: Feature::ShellSnapshot 与 FEATURES 注册表

rust
/// Experimental shell snapshotting.
ShellSnapshot,

FeatureSpec {
    id: Feature::ShellSnapshot,
    key: "shell_snapshot",
    stage: Stage::Stable,
    default_enabled: true,
},

这里的 Stable 只描述 feature stage;是否真正产生文件,还要经过 Session 配置、environment 是否 remote、 当前 shell 是否解析成功,以及快照脚本能否写入并通过验证。

2. Session配置 ​

Session 构造时同时决定 legacy host snapshot 是否启用,以及是否由 executor 侧的 ShellSnapshotV2 接管。 当 ShellSnapshotV2、Shell 工具和 Unified Exec 同时启用且 shell mode 为 Direct 时,Session 不创建本地 legacy 文件;环境解析会把 executor capability 标记为 shell_snapshot_v2_supported,后续由执行端按请求生成快照。 旧 ShellSnapshot 路径仍用于本地 user shell 和不满足 V2 条件的环境。

源码位置:codex-rs/core/src/session/session.rs :: Session::spawn_internal 的 shell snapshot 分支

rust
let shell_snapshot = if config.features.enabled(Feature::ShellSnapshot) {
    ShellSnapshot::new(
        config.codex_home.clone(),
        thread_id,
        session_telemetry.clone(),
        state_db_ctx.clone(),
    )
} else {
    ShellSnapshot::disabled()
};

let turn_environments = Arc::new(ThreadEnvironments::new(
    environment_manager,
    default_shell.clone(),
    session_configuration.inferred_environment_config(),
    shell_snapshot,
    inherited_environments.unwrap_or_default(),
    config.features.enabled(Feature::DeferredExecutor),
));

ShellSnapshot::disabled() 不是错误对象,它让后续环境解析仍能继续,只是每个 TurnEnvironment 得不到 snapshot future。这个设计使 feature 关闭与快照生成失败都能退回“按当前环境执行命令”,但二者的原因不同:前者是配置选择, 后者是运行时构造失败。

源码位置:codex-rs/core/src/shell_snapshot.rs :: ShellSnapshot

rust
#[derive(Clone)]
pub(crate) struct ShellSnapshot {
    config: Option<Arc<ShellSnapshotConfig>>,
}

struct ShellSnapshotConfig {
    codex_home: AbsolutePathBuf,
    session_id: ThreadId,
    session_telemetry: SessionTelemetry,
    state_db: Option<StateDbHandle>,
}

pub(crate) struct ShellSnapshotFile {
    path: AbsolutePathBuf,
}

ShellSnapshot 可 clone,是因为它只携带共享配置;ShellSnapshotFile 代表已经成功生成的具体文件,并且拥有 文件删除的 Drop 责任。两者不能在文章中统称为“快照对象”。

3. 环境绑定 ​

环境解析阶段才知道三个决定性条件:environment 是否 remote、cwd 能否转成本地绝对路径,以及 shell 是否能从 本地或远端 environment 信息中解析出来。远端 environment 不使用本地 snapshot 文件;如果 executor 报告 shell_snapshot_v2 capability,则由 executor 侧处理 V2 请求,因为本地 Codex 进程不能替远端 shell 捕获 rc 文件。

源码位置:codex-rs/core/src/environment_selection.rs :: ThreadEnvironments::resolve_environment

rust
let shell = if environment.is_remote() {
    match environment.info().await {
        Ok(info) => match Shell::from_environment_shell_info(info.shell) {
            Ok(shell) => Some(shell),
            Err(err) => {
                tracing::warn!("failed to resolve shell for `{environment_id}`: {err}");
                None
            }
        },
        Err(err) => {
            tracing::warn!("failed to get environment info: {err}");
            None
        }
    }
} else {
    Some(local_shell)
};

let task = shell_snapshot
    .build(Arc::clone(&environment), selection.cwd, shell.clone())
    .boxed()
    .shared();
drop(tokio::spawn(task.clone()));

Ok(ResolvedEnvironment {
    environment,
    shell,
    shell_snapshot: task,
    shell_snapshot_v2_supported,
})

这里的 Shared 很关键:环境解析只启动一次构造 future,后续 Step 可以共享同一结果,而不是每个命令都重跑 用户 rc 文件。drop(tokio::spawn(...)) 只丢弃 JoinHandle,不会取消共享 future;真正的结果由 Shared future 和后续 TurnEnvironment consumer 持有。

源码位置:codex-rs/core/src/environment_selection.rs :: ThreadEnvironments::snapshot 与 TurnEnvironment::shell_snapshot

rust
pub(crate) fn shell_snapshot(&self, cwd: &AbsolutePathBuf) -> Option<AbsolutePathBuf> {
    if self.cwd != PathUri::from_abs_path(cwd) {
        return None;
    }
    self.shell_snapshot
        .peek()?
        .as_deref()
        .map(ShellSnapshotFile::path)
}

peek() 不等待尚未完成的 future。因此命令启动时如果 snapshot 还没完成,调用方会得到 None,而不是阻塞命令 等待快照;但 Session 启动预热通常已经给它提前创建机会。这个选择把“命令是否等待”交给上层 runtime,而不是 让 TurnEnvironment 的查询隐式变成阻塞操作。

4. 生成流程 ​

build 只负责环境和 cwd 门控,try_create 才负责命名、清理旧文件、写临时文件、验证和 rename。文件名包含 session id、纳秒 nonce 和 shell 脚本扩展名;临时文件使用 .tmp- 前缀,避免半成品被当成正式 snapshot。

源码位置:codex-rs/core/src/shell_snapshot.rs :: ShellSnapshot::build 与 ShellSnapshot::try_create

rust
pub(crate) async fn build(
    self,
    environment: Arc<Environment>,
    cwd: PathUri,
    shell: Option<Shell>,
) -> Option<Arc<ShellSnapshotFile>> {
    let config = self.config.as_ref()?;
    if environment.is_remote() {
        return None;
    }

    let shell = shell?;
    let cwd = cwd.to_abs_path().ok()?;
    Self::build_for_cwd(Arc::clone(config), cwd, shell).await
}

let extension = match shell.shell_type {
    ShellType::PowerShell => "ps1",
    _ => "sh",
};
let nonce = SystemTime::now()
    .duration_since(SystemTime::UNIX_EPOCH)
    .map(|duration| duration.as_nanos())
    .unwrap_or(0);
let path = codex_home
    .join(SNAPSHOT_DIR)
    .join(format!("{session_id}.{nonce}.{extension}"));
let temp_path = codex_home
    .join(SNAPSHOT_DIR)
    .join(format!("{session_id}.tmp-{nonce}"));

生成前会异步调用 cleanup_stale_snapshots。这意味着清理与本次生成不是一个锁住目录的事务:旧文件清理失败 只写 warning,不阻止新 snapshot 尝试。

源码位置:codex-rs/core/src/shell_snapshot.rs :: ShellSnapshot::try_create

rust
if let Err(err) = write_shell_snapshot(shell.shell_type, &temp_path, session_cwd).await {
    tracing::warn!("Failed to create shell snapshot for {}: {err:?}", shell.name());
    return Err("write_failed");
}

if let Err(err) = validate_snapshot(shell, &temp_path, session_cwd).await {
    tracing::error!("Shell snapshot validation failed: {err:?}");
    remove_snapshot_file(&temp_path).await;
    return Err("validation_failed");
}

if let Err(err) = fs::rename(&temp_path, &path).await {
    tracing::warn!("Failed to finalize shell snapshot: {err:?}");
    remove_snapshot_file(&temp_path).await;
    return Err("write_failed");
}

Ok(ShellSnapshotFile { path })

当前源码的失败顺序是:写失败不进入验证;验证失败删除临时文件;rename 失败也删除临时文件;只有 rename 成功后才产生可被 ShellSnapshotFile::path() 返回的正式文件。

5. 脚本内容 ​

脚本捕获的不是任意 stdout,而是从 # Snapshot file 标记开始的恢复部分。标记之前允许 shell 启动 rc 文件、 插件或环境脚本产生输出;strip_snapshot_preamble 把这些启动噪声排除,避免它们在未来命令中被当作 shell 语句。

源码位置:codex-rs/core/src/shell_snapshot.rs :: write_shell_snapshot、capture_snapshot、strip_snapshot_preamble

rust
async fn write_shell_snapshot(
    shell_type: ShellType,
    output_path: &AbsolutePathBuf,
    cwd: &AbsolutePathBuf,
) -> Result<()> {
    if shell_type == ShellType::PowerShell || shell_type == ShellType::Cmd {
        bail!("Shell snapshot not supported yet for {shell_type:?}");
    }
    let shell = get_shell(shell_type, /*path*/ None)
        .with_context(|| format!("No available shell for {shell_type:?}"))?;

    let raw_snapshot = capture_snapshot(&shell, cwd).await?;
    let snapshot = strip_snapshot_preamble(&raw_snapshot)?;
    if let Some(parent) = output_path.parent() {
        fs::create_dir_all(parent).await?;
    }
    fs::write(output_path, snapshot).await?;
    Ok(())
}

fn strip_snapshot_preamble(snapshot: &str) -> Result<String> {
    let marker = "# Snapshot file";
    let Some(start) = snapshot.find(marker) else {
        bail!("Snapshot output missing marker {marker}");
    };
    Ok(snapshot[start..].to_string())
}

这里有一个必须直说的版本边界:源码保留了 powershell_snapshot_script(),但 write_shell_snapshot 在进入 capture_snapshot 前就拒绝 PowerShell 和 Cmd;当前 Windows 端到端 snapshot 测试也被标记为 ignored。因此本篇 只把 POSIX shell 的“当前可达路径”作为已实现主线,不能把 PowerShell 函数存在写成 Windows 已经可用。

5.1 POSIX内容 ​

Zsh、Bash 和 Sh 的脚本都先加载相应的初始化文件,再输出 snapshot marker、清理 alias、函数、shell options 和 exports。PWD/OLDPWD 被排除,因为命令 cwd 由 runtime 单独设置,不能被旧 snapshot 的目录变量覆盖。

源码位置:codex-rs/core/src/shell_snapshot.rs :: zsh_snapshot_script 与 bash_snapshot_script

bash
if [[ -n "$ZDOTDIR" ]]; then
  rc="$ZDOTDIR/.zshrc"
else
  rc="$HOME/.zshrc"
fi
[[ -r "$rc" ]] && . "$rc"
print '# Snapshot file'
print '# Unset all aliases to avoid conflicts with functions'
unalias -a 2>/dev/null || true
print '# Functions'
functions
print ''
setopt | sed 's/^/setopt /'
print ''
alias -L

Bash 的实现使用 declare -f、set -o、alias -p 和 compgen -e,Sh 则对 typeset/declare、alias 和 export -p 做能力探测。因此“快照有统一格式”不等于“所有 shell 的捕获命令完全相同”;统一的是 marker 和 可 source 的结果,采集方式由 shell 类型决定。

6. 命令包装 ​

快照文件不会直接替代用户命令。它只在本地、snapshot 存在、命令参数至少包含程序/-lc/脚本三部分时参与 包装。包装器先保存需要在 source 后恢复的变量,再 source snapshot,随后重新导出 Codex 运行时覆盖,最后 用原 shell 执行原脚本。

源码位置:codex-rs/core/src/tools/runtimes/mod.rs :: maybe_wrap_shell_lc_with_snapshot

rust
if cfg!(windows) {
    return command.to_vec();
}
let Some(snapshot) = shell_snapshot else {
    return command.to_vec();
};
if !snapshot.exists() || command.len() < 3 {
    return command.to_vec();
}
if command[1].as_str() != "-lc" {
    return command.to_vec();
}

let original_shell = shell_single_quote(&command[0]);
let original_script = shell_single_quote(&command[2]);
let snapshot_path = shell_single_quote(snapshot.to_string_lossy().as_ref());
let mut override_env = explicit_env_overrides.clone();
for key in [CODEX_THREAD_ID_ENV_VAR, CODEX_PERMISSION_PROFILE_ENV_VAR] {
    if let Some(value) = env.get(key) {
        override_env.insert(key.to_string(), value.clone());
    }
}
let (override_captures, override_exports) = build_override_exports(
    &override_env,
    &[CODEX_PERMISSION_PROFILE_ENV_VAR],
);
let (proxy_captures, proxy_exports) = build_proxy_env_exports(env);
let runtime_path_prepend_exports =
    runtime_path_prepends.shell_exports_after_snapshot(explicit_env_overrides);

源码位置:codex-rs/core/src/tools/runtimes/mod.rs :: maybe_wrap_shell_lc_with_snapshot 的最终命令构造

rust
let rewritten_script = format!(
    "{override_captures}\n\nif . '{snapshot_path}' >/dev/null 2>&1; then :; fi\n\n{override_exports}\n\n{runtime_path_prepend_exports}\n\nexec '{original_shell}' -c '{original_script}'{trailing_args}"
);

vec![shell_path.to_string(), "-c".to_string(), rewritten_script]

这个顺序解决了两个看似冲突的要求:用户 shell 的 PATH、函数和 alias 要恢复;Codex 的权限 profile、thread id、 代理和运行时 PATH 又必须覆盖旧值。source 失败不会阻止原 shell 被 exec,因为 source 被包在 if ...; then :; fi 中;因此 snapshot 文件消失或内容失效时,命令仍有机会按普通 shell 路径执行。

7. 两种执行者 ​

user shell 与 Unified Exec 都消费 TurnEnvironment.shell_snapshot(cwd),但 0.150.0 已删除旧的 runtimes/shell.rs。本地 user shell 仍在 task runtime 中调用 wrapper;Unified Exec 还会根据 ShellSnapshotV2 capability 构造 ShellSnapshotRequest,把快照生成交给 executor。

源码位置:codex-rs/core/src/tasks/user_shell.rs :: prepare_user_shell_exec_command

rust
let command = prepare_user_shell_exec_command_with_path_prepend(
    &display_command,
    environment_shell,
    shell_snapshot_location.as_ref(),
    &shell_environment_policy.r#set,
    &mut exec_env_map,
    apply_package_path_prepend,
);

源码位置:codex-rs/core/src/unified_exec/shell_snapshot.rs :: shell_snapshot_request

rust
if !context.session.features().enabled(Feature::ShellSnapshotV2)
    || !request.turn_environment.shell_snapshot_v2_supported
    || request.turn_environment.selection.cwd != *cwd
    || !matches!(request.shell_mode, UnifiedExecShellMode::Direct)
    || !matches!(request.shell_type, ShellType::Bash | ShellType::Zsh | ShellType::Sh)
    || request.command.get(1).is_none_or(|flag| flag != "-lc")
{
    return None;
}

Some(ShellSnapshotRequest {
    scope_id: format!(
        "{}:{}",
        context.session.thread_id(),
        request.turn_environment.selection.environment_id
    ),
    shell: ShellInfo {
        name: request.shell_type.name().to_string(),
        path: request.command.first()?.clone(),
    },
})

ShellSnapshot 负责恢复 shell 语义,不负责审批、文件系统限制或网络代理。两个 runtime 仍先经过 SandboxAttempt、环境变量策略和 managed network 处理,再把最终 command 交给 snapshot wrapper;这就是为什么 “命令 source 了用户环境”不能被解释成“命令绕过了 sandbox”。

7.1 V2执行端快照 ​

V2 与 legacy 文件快照的差异是“快照在哪里生成”。shell_snapshot_request() 只在功能开关、executor capability、cwd、Direct shell mode、POSIX shell 和 -lc 参数全部满足时返回请求;否则 Unified Exec 继续使用普通命令路径。执行端收到 ShellSnapshotRequest 后可以在自己的 filesystem 中生成内存快照, 所以测试 shell_snapshot_v2_filters_profile_secrets_without_creating_files 特别断言 Codex host 没有 创建 shell_snapshots 文件。

源码位置:codex-rs/core/src/unified_exec/shell_snapshot.rs :: shell_snapshot_request

8. 文件生命周期 ​

成功的 ShellSnapshotFile 由 Arc 传入 TurnEnvironment 和后续命令 runtime。最后一个引用释放时执行 Drop, 同步删除正式文件。临时文件不依赖 Drop,而是在验证或 rename 失败分支显式删除。

源码位置:codex-rs/core/src/shell_snapshot.rs :: Drop for ShellSnapshotFile

rust
impl Drop for ShellSnapshotFile {
    fn drop(&mut self) {
        if let Err(err) = std::fs::remove_file(&self.path) {
            tracing::warn!(
                "Failed to delete shell snapshot at {:?}: {err:?}",
                self.path
            );
        }
    }
}

这解释了两个观察结果:Session shutdown 后文件通常消失,但如果外部代码仍持有 Arc<ShellSnapshotFile>,Drop 不会提前发生;删除失败只产生 warning,不会把已经完成的命令重新标记为失败。

9. 过期清理 ​

Drop 只覆盖当前进程仍持有的正式文件,不能清理崩溃或强制退出留下的文件。因此每次创建新 snapshot 时还会 异步扫描 shell_snapshots:活动 session id 豁免;无法解析的文件名、找不到对应 rollout 的 session,以及 rollout 超过三天未更新的快照都会被删除。

源码位置:codex-rs/core/src/shell_snapshot.rs :: cleanup_stale_snapshots

rust
let active_session_id = active_session_id.to_string();
while let Some(entry) = entries.next_entry().await? {
    if !entry.file_type().await?.is_file() {
        continue;
    }
    let path = entry.path();
    let file_name = entry.file_name().to_string_lossy();
    let Some(session_id) = snapshot_session_id_from_file_name(&file_name) else {
        remove_snapshot_file(&path).await;
        continue;
    };
    if session_id == active_session_id {
        continue;
    }
    let rollout_path =
        find_thread_path_by_id_str(codex_home, session_id, state_db.as_deref()).await?;
    let Some(rollout_path) = rollout_path else {
        remove_snapshot_file(&path).await;
        continue;
    };
    let modified = fs::metadata(&rollout_path).await?.modified()?;
    if now.duration_since(modified).ok().is_some_and(|age| age >= SNAPSHOT_RETENTION) {
        remove_snapshot_file(&path).await;
    }
}

state_db 不是快照的持久化内容,它只是帮助清理逻辑找到 session 对应的 rollout。快照本身是临时运行资源, 不是 resume 时需要恢复的协议 item。

10. 失败边界 ​

现象源码边界结果
feature 关闭ShellSnapshot::disabled不创建 snapshot,runtime 直接执行命令
remote environmentShellSnapshot::build返回 None,远端执行器负责自己的 shell 环境
cwd 无法转绝对路径PathUri::to_abs_path当前 environment 不提供本地 snapshot
shell 未解析shell? 或 environment shell info 错误snapshot 不可用,命令走普通路径或由上层报错
PowerShell/Cmdwrite_shell_snapshot 明确 bail!当前入口返回 write_failed,不能宣称已生成 Windows 快照
marker 缺失strip_snapshot_preamble写入前验证失败,不保留正式文件
snapshot 文件被删除runtime wrapper snapshot.exists()不包装命令,继续普通 shell 执行
source 失败wrapper 的 if . snapshot; then :; fi继续执行原 shell,不能把 source 失败误读为命令成功
rollout 不存在/过期cleanup_stale_snapshots清理旧文件;活动 session 不受影响

这些路径说明 snapshot 是“尽力恢复用户 shell 语义”的辅助资源,不是命令执行的唯一入口,也不是安全边界。 即使 snapshot 缺失,命令仍可能成功;即使 snapshot 成功,命令仍然要经过 sandbox、approval、环境和网络策略。

11. 测试路径 ​

第一组测试直接验证脚本输出,而不启动完整 Session:

源码位置:codex-rs/core/src/shell_snapshot_tests.rs :: strip_snapshot_preamble_requires_marker 与 POSIX snapshot tests

rust
#[test]
fn strip_snapshot_preamble_requires_marker() {
    let result = strip_snapshot_preamble("missing header");
    assert!(result.is_err());
}

#[cfg(unix)]
#[test]
fn bash_snapshot_filters_invalid_exports() -> Result<()> {
    let output = Command::new("/bin/bash")
        .arg("-c")
        .arg(bash_snapshot_script())
        .env("BASH_ENV", "/dev/null")
        .env("VALID_NAME", "ok")
        .env("PWD", "/tmp/stale")
        .env("BAD-NAME", "broken")
        .output()?;

    assert!(output.status.success());
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("VALID_NAME"));
    assert!(!stdout.contains("PWD=/tmp/stale"));
    assert!(!stdout.contains("BAD-NAME"));
    Ok(())
}

这组测试证明 marker 是必要结构、环境变量名会过滤非法名称、PWD 不进入快照;不证明实际命令 runtime 已经 source 文件,也不覆盖真实用户 rc 文件的所有副作用。

第二组测试通过 TestCodexHarness,让模型发出真实 shell tool call,再观察 command begin/end、snapshot 文件和 命令输出。

源码位置:codex-rs/core/tests/suite/shell_snapshot.rs :: linux_unified_exec_uses_shell_snapshot 与 linux_shell_command_uses_shell_snapshot

rust
let run = run_snapshot_command("echo snapshot-linux").await?;
let stdout = normalize_newlines(&run.end.stdout);

assert_eq!(run.begin.command.get(1).map(String::as_str), Some("-lc"));
assert_eq!(run.begin.command.get(2).map(String::as_str), Some("echo snapshot-linux"));
assert!(run.snapshot_path.starts_with(&run.codex_home));
assert_posix_snapshot_sections(&run.snapshot_content);
assert_eq!(run.end.exit_code, 0);
assert!(stdout.contains("snapshot-linux"));

它证明本地 Linux runtime 的 snapshot 文件能被命令路径消费,并且命令正常结束;不证明 remote environment、 Windows 当前入口或所有 sandbox backend 都能使用相同文件。

第三组测试验证生命周期清理:

源码位置:codex-rs/core/tests/suite/shell_snapshot.rs :: shell_snapshot_deleted_after_shutdown_with_skills

rust
let snapshot_path = wait_for_snapshot(&codex_home).await?;
assert!(snapshot_path.exists());

codex.submit(Op::Shutdown {}).await?;
wait_for_event(&codex, |ev| matches!(ev, EventMsg::ShutdownComplete)).await;

drop(codex);
drop(harness);
sleep(Duration::from_millis(150)).await;
assert!(!snapshot_path.exists());

它把“文件曾经生成”和“shutdown 后 owner 已释放”分成两个断言;不证明操作系统在进程被 kill -9 时仍能执行 Drop,因此崩溃遗留文件仍由下一次创建时的 stale cleanup 负责。

12. 验证方法 ​

在本版本源码对应的 codex-rs workspace 中运行:

bash
cargo test -p codex-core shell_snapshot
cargo test -p codex-core user_shell_snapshot_preserves_package_path_prepend
cargo test -p codex-core --test all linux_unified_exec_uses_shell_snapshot
cargo test -p codex-core --test all linux_shell_command_uses_shell_snapshot

只读搜索练习:

  1. 从 Session::spawn_internal 找到 Feature::ShellSnapshot,说明为什么 Session 保存的是 builder,而不是文件路径。
  2. 沿 resolve_environment → ShellSnapshot::build → TurnEnvironment::shell_snapshot,指出 remote environment 和 cwd 不匹配分别在哪一层返回 None。
  3. 在 maybe_wrap_shell_lc_with_snapshot 中列出 source 前捕获、source 后恢复的两组变量,解释为什么 runtime_path_prepends 不能直接写入旧 snapshot。
  4. 对照 write_shell_snapshot 和 ignored 的 Windows 集成测试,说明为什么当前源码不能宣称 PowerShell snapshot 已完成。
  5. 修改测试中的 snapshot 文件使 marker 缺失,预测 validation_failed;再删除正式文件,预测 runtime 回到普通 command path。最后分别在 try_create、maybe_wrap_shell_lc_with_snapshot 和 cleanup_stale_snapshots 找到 三个不同的恢复边界。

13. 能力边界 ​

Shell snapshot 只保存 shell 进程能够导出的内容,并不保存一个可恢复的 Codex Session;它不包含 Turn history、 ModelInfo、审批状态或工具 registry。它也不替代 sandbox:source 恢复了用户 shell 的函数和 PATH 后,命令仍由 后续 runtime 应用权限、网络和平台执行约束。

当前版本的 POSIX 路径由源码和 Linux/macOS 测试支持;PowerShell/Cmd 的生成函数存在,但入口 guard、Windows 测试 ignored 和 runtime 分支共同表明它仍不是可宣称完成的公开能力。遇到命令环境异常时,应先判断是 snapshot 没有生成、snapshot 没有被 wrapper 采用、source 后 override 覆盖了变量,还是 sandbox/runtime 在更后面修改了 环境;不要只搜索 ShellSnapshot 一个类型名。