{"record":{"id":"5ce29c8a96d86a60","repo":"Hmbown/CodeWhale","slug":"patch-path-must-be-workspace-relative","errorCode":null,"errorMessage":"patch path must be workspace-relative","messagePattern":"patch path must be workspace-relative","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/eval.rs","lineNumber":690,"sourceCode":"    content.push_str(line);\n    content.push('\\n');\n    fs::write(path, content).with_context(|| format!(\"failed to write {}\", path.display()))\n}\n\nfn apply_patch(root: &Path, patch: &str) -> Result<()> {\n    let mut lines = patch.lines();\n\n    let begin = lines.next().unwrap_or_default();\n    if begin != \"*** Begin Patch\" {\n        return Err(anyhow!(\"patch missing *** Begin Patch header\"));\n    }\n\n    let header = lines.next().unwrap_or_default();\n    let file_rel = header\n        .strip_prefix(\"*** Update File: \")\n        .ok_or_else(|| anyhow!(\"only *** Update File patches are supported\"))?;\n    if file_rel.contains(\"..\") {\n        return Err(anyhow!(\"patch path must be workspace-relative\"));\n    }\n\n    let file_path = root.join(file_rel);\n    let original = read_workspace_file(&file_path)?;\n    let had_trailing_newline = original.ends_with('\\n');\n    let mut file_lines: Vec<String> = original.lines().map(|l| l.to_string()).collect();\n\n    let mut cursor = 0usize;\n    for raw_line in lines {\n        if raw_line == \"*** End Patch\" {\n            break;\n        }\n        if raw_line.starts_with(\"*** \") {\n            return Err(anyhow!(\"unexpected patch directive: {raw_line}\"));\n        }\n        if raw_line.starts_with(\"@@\") {\n            continue;\n        }","sourceCodeStart":672,"sourceCodeEnd":708,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/crates/tui/src/eval.rs#L672-L708","documentation":"The path after `*** Update File:` must stay inside the eval workspace: any occurrence of `..` in the relative path is rejected before the file is read (eval.rs:690). This is the sandbox-escape guard for patch targets.","triggerScenarios":"The patch references `../outside.txt`, `a/../../b`, or any parent traversal; the model tries to edit a file that lives above the eval workspace root.","commonSituations":"Model reasons about absolute repo paths and emits them as relative paths with `..`; eval root configured one level below the files the model was shown.","solutions":["Move the target file inside the workspace, or reconfigure the eval root so the target is contained","Emit a path relative to the workspace root with no parent-directory segments"],"exampleFix":"*** before\n*** Update File: ../shared/config.toml\n*** after (root the workspace one level up, or reference an in-workspace copy)\n*** Update File: shared/config.toml","handlingStrategy":"validation","validationCode":"let rel = header\n    .strip_prefix(\"*** Update File: \")\n    .context(\"not an update directive\")?;\nanyhow::ensure!(!rel.contains(\"..\"), \"patch escapes workspace: {rel}\");","typeGuard":"fn patch_path_is_in_workspace(rel: &str) -> bool {\n    !rel.contains(\"..\")\n}","tryCatchPattern":null,"preventionTips":["Tell the model every path is workspace-relative","Keep the sandbox root equal to the directory tree the model sees","Reject parent traversal at the tool boundary before applying"],"tags":["patch","security","path","sandbox"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}