From be16daecaef9e7078ccfbe09fc5d422ad51ca8db Mon Sep 17 00:00:00 2001 From: Abhigyan Patwari <126312502+abhigyanpatwari@users.noreply.github.com> Date: Mon, 20 Jul 2026 15:55:49 +0530 Subject: [PATCH] fix(eval): pre-approve proposer tools under Claude Code 2.1.214 ENV_SCRUB hardening (#2576) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first real skill-evolution run got past task binding, then the proposer session exited 1 with "Permission mode forced to default — CLAUDE_CODE_SUBPROCESS_ENV_SCRUB is set (allowed_non_write_users hardening)". On 2.1.214 the permission resolver unconditionally forces permission mode to "default" whenever CLAUDE_CODE_SUBPROCESS_ENV_SCRUB is set — `--permission-mode dontAsk`, settings `permissions.defaultMode`, and `autoAllowBashIfSandboxed` are all ignored for the mode decision. The proposer runs headless `-p --bare` where Bash is its only writable tool (it writes the candidate overlay); under forced "default" Bash was no longer auto-approved, so the session blocked. We cannot set ENV_SCRUB=0 (it scrubs the Anthropic auth token from the sandboxed proposer's Bash subprocesses). Instead, align with the forced mode: pre-approve the proposer's exact tool surface via settings `permissions.allow` (["Read","Grep","Glob","Bash"]) — under "default" a tool runs without a prompt iff it matches an allow rule — and stop requesting a non-default mode so no warning fires. ENV_SCRUB and the full sandbox filesystem/network lockdown are unchanged. The real-binary containment canary is updated to the new invocation (no --permission-mode) so the CI job is the authoritative empirical gate, and a fast unit assertion pins the new permissions.allow / absent defaultMode. Claude-Session: https://claude.ai/code/session_01Va5uu9Ar3e45QZ5xFsG4AZ Co-authored-by: Gergo Magyar Co-authored-by: Claude Opus 4.8 --- eval/tests/test_proposer_sandbox.py | 11 +++++++++-- eval/workflow_bench/evolve.py | 5 ++++- eval/workflow_bench/proposer_sandbox.py | 9 ++++++++- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/eval/tests/test_proposer_sandbox.py b/eval/tests/test_proposer_sandbox.py index 338eead52..e5ceb7b2c 100644 --- a/eval/tests/test_proposer_sandbox.py +++ b/eval/tests/test_proposer_sandbox.py @@ -58,6 +58,11 @@ def test_environment_is_allowlisted_and_shell_children_are_credential_free(monke assert settings["sandbox"]["failIfUnavailable"] is True assert settings["sandbox"]["allowUnsandboxedCommands"] is False assert settings["sandbox"]["network"]["deniedDomains"] == ["*"] + # ENV_SCRUB forces "default" mode; the proposer's tools (Bash writes the + # overlay) run headless only because they are explicitly pre-approved. + # Requesting a non-default defaultMode would merely warn, so it must be gone. + assert settings["permissions"]["allow"] == ["Read", "Grep", "Glob", "Bash"] + assert "defaultMode" not in settings["permissions"] @pytest.mark.parametrize( @@ -716,8 +721,10 @@ for line in sys.stdin: "--strict-mcp-config", "--mcp-config", mcp_config, - "--permission-mode", - "dontAsk", + # No --permission-mode: mirrors production (run_proposer). + # ENV_SCRUB forces "default"; Bash runs only because + # settings permissions.allow pre-approves it. This is the + # authoritative empirical gate for that behavior. "--model", "claude-canary-20260718", "--allowedTools", diff --git a/eval/workflow_bench/evolve.py b/eval/workflow_bench/evolve.py index 8cf6be495..d917abe88 100644 --- a/eval/workflow_bench/evolve.py +++ b/eval/workflow_bench/evolve.py @@ -501,7 +501,10 @@ def run_proposer( auth_token=args.auth_token, base_url=args.base_url, ), - permission_mode="dontAsk", + # No permission_mode: CLAUDE_CODE_SUBPROCESS_ENV_SCRUB + # forces "default", so requesting dontAsk only warns. Tools + # are pre-approved via settings permissions.allow + # (proposer_sandbox.build_claude_settings). command_prefix=sandbox.command_prefix, require_pid_namespace=True, bare=True, diff --git a/eval/workflow_bench/proposer_sandbox.py b/eval/workflow_bench/proposer_sandbox.py index 3e492fee9..254906139 100644 --- a/eval/workflow_bench/proposer_sandbox.py +++ b/eval/workflow_bench/proposer_sandbox.py @@ -329,7 +329,14 @@ def build_claude_settings() -> str: }, }, "permissions": { - "defaultMode": "dontAsk", + # CLAUDE_CODE_SUBPROCESS_ENV_SCRUB forces permission mode to + # "default" (allowed_non_write_users hardening), so requesting a + # non-default mode only emits a warning and never takes effect. + # Under "default" a tool runs without a prompt only if it matches an + # allow rule, so pre-approve the proposer's exact tool surface. Bash + # is the only writable tool under --bare (it writes the candidate + # overlay) and stays sandbox-confined by the sandbox.* policy above. + "allow": ["Read", "Grep", "Glob", "Bash"], "disableBypassPermissionsMode": "disable", }, "env": {