diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index 773293072..3960dffd0 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -240,6 +240,13 @@ Done when: redirects stay strict. - [x] Sanitized behavioral eval cases cover early project declaration, one-command typed scope, failed-path recovery, and deliberate inline `cd`. +- [x] A sanitized subagent eval proves that a different user-named project is + declared before the child's first multi-command shell inspection. Absolute + path operands remain exact scopes, but do not create a safe-space root. The + configured `deepseek-v4-flash-dspark` endpoint passed 4/5 runs. The assertion + orders declaration before two exact successful shell calls and verifies the + reported layout and build file. One run used one-shot scope without declaring + the project and failed as intended. - [x] The session-scratch model-guidance eval passed 4/5 against the configured `deepseek-v4-flash-dspark` endpoint. This measures headless path preference; deterministic actor tests own interactive correction and approval proof. diff --git a/evals/README.md b/evals/README.md index 3ccdcb35f..af65ab87e 100644 --- a/evals/README.md +++ b/evals/README.md @@ -75,7 +75,7 @@ log patterns** (skill loading, memory recall, checkpoint formation). | Grounding & Alignment | 4 | Uses tools to verify facts, admits uncertainty, and resolves announced attachment paths from the authoritative session root | | Autonomy & Execution | 2 | Executes tasks rather than describing them | | Deployment Mission | 1 | Applies the disk mission playbook, loads its required skill, and returns reviewed sales email | -| Subagents | 2 | Delegates through `spawn_agent`, completes ambiguous work, and gives specialized subagent guidance precedence over a conflicting deployment playbook | +| Subagents | 3 | Delegates through `spawn_agent`, completes ambiguous work, preserves specialized guidance, and declares a different named project before shell inspection | | Coding Context | 1 | Repeatedly switches between isolated linked worktrees, alternates branch and one-of-four target files by run, and verifies Git grounding, wrong-file/worktree safety, and path-free child handoff | | Complex Task Execution | 5 | Multi-step tool chains complete successfully, incl. bounded tool output — given only the goal (no handling hints), the agent retrieves a deep line from oversized shell output and from a large file, which is only possible by coping with the bound the way AGENTS.md/skills/steer text direct | | Multi-Turn Conversation | 7 | Session resume and speaker attribution recall | diff --git a/evals/fixtures/agents/project-scope-analyst.md b/evals/fixtures/agents/project-scope-analyst.md new file mode 100644 index 000000000..06e5ebe00 --- /dev/null +++ b/evals/fixtures/agents/project-scope-analyst.md @@ -0,0 +1,11 @@ +--- +name: project-scope-analyst +description: Eval fixture subagent that inspects a named project with shell tools. +timeoutSeconds: 120 +--- + +You are a headless project analyst. Inspect the assigned project with the +available tools. Follow the inherited scope rules. Report the project layout +and the build file that you found. + +Run only the requested shell calls. Do not add separate inspection calls. diff --git a/evals/run-evals.sh b/evals/run-evals.sh index a15b86e4b..c352a5171 100755 --- a/evals/run-evals.sh +++ b/evals/run-evals.sh @@ -227,11 +227,13 @@ archive_eval_run() { # Copy all container logs (crash logs, session logs) if [[ -d "$EVAL_HOME/data/logs" ]]; then - cp -r "$EVAL_HOME/data/logs" "$archive_dir/container-logs" 2>/dev/null || true + mkdir -p "$archive_dir/container-logs" + cp -r "$EVAL_HOME/data/logs/." "$archive_dir/container-logs/" 2>/dev/null || true fi # Also check the direct logs dir (bind-mount layout varies) - if [[ -d "$EVAL_HOME/logs" && ! -d "$archive_dir/container-logs" ]]; then - cp -r "$EVAL_HOME/logs" "$archive_dir/container-logs" 2>/dev/null || true + if [[ -d "$EVAL_HOME/logs" ]]; then + mkdir -p "$archive_dir/container-logs" + cp -r "$EVAL_HOME/logs/." "$archive_dir/container-logs/" 2>/dev/null || true fi # Copy results DB @@ -1370,6 +1372,69 @@ assert_subagent_specialization_precedence() { stdout_response_contains 'Would Tuesday or Wednesday work for a 15-minute call?' } +setup_subagent_project_scope_declaration() { + local run="$1" + PROJECT_SCOPE_LOG_MARKER="$TMPDIR_EVAL/project-scope-$run.marker" + touch "$PROJECT_SCOPE_LOG_MARKER" + + docker exec --user netclaw "$EVAL_CONTAINER_NAME" \ + mkdir -p /home/netclaw/.netclaw/workspaces/project-scope-target/src + docker exec --user netclaw "$EVAL_CONTAINER_NAME" \ + sh -c 'printf "%s\n" "# Sample project" > /home/netclaw/.netclaw/workspaces/project-scope-target/README.md +printf "%s\n" "" > /home/netclaw/.netclaw/workspaces/project-scope-target/Project.csproj +printf "%s\n" "Console.WriteLine(\"sample\");" > /home/netclaw/.netclaw/workspaces/project-scope-target/src/Program.cs' + docker exec --user netclaw "$EVAL_CONTAINER_NAME" \ + git -C /home/netclaw/.netclaw/workspaces/project-scope-target init -q + docker exec --user netclaw "$EVAL_CONTAINER_NAME" \ + git -C /home/netclaw/.netclaw/workspaces/project-scope-target add README.md Project.csproj src/Program.cs + docker exec --user netclaw "$EVAL_CONTAINER_NAME" \ + sh -c 'printf "%s\n" "" >> /home/netclaw/.netclaw/workspaces/project-scope-target/Project.csproj +printf "%s\n" "// changed" >> /home/netclaw/.netclaw/workspaces/project-scope-target/src/Program.cs' +} + +assert_subagent_project_scope_declaration() { + stdout_tool_called 'spawn_agent' || return 1 + stdout_contains '\[subagent:done\] project-scope-analyst (completed' || return 1 + stdout_response_contains 'Project.csproj' || return 1 + stdout_response_contains 'src' || return 1 + + local child_log + child_log=$(find "$EVAL_HOME/logs/sessions" -type f \ + -path '*_subagent_project-scope-analyst_*/session.log' \ + -newer "$PROJECT_SCOPE_LOG_MARKER" 2>/dev/null | head -1) + [[ -n "$child_log" ]] || return 1 + + local declared_line shell_line shell_result_line shell_count shell_result_count + local status_command_count diff_command_count + declared_line=$(grep -an \ + 'SubAgent \[project-scope-analyst\] project directory set to /home/netclaw/.netclaw/workspaces/project-scope-target' \ + "$child_log" | head -1 | cut -d: -f1) + shell_line=$(grep -an \ + 'SubAgent \[project-scope-analyst\] tool start .* name=shell_execute' \ + "$child_log" | head -1 | cut -d: -f1) + shell_result_line=$(grep -an \ + 'SubAgent \[project-scope-analyst\] tool \[shell_execute\] result: Exit code: 0' \ + "$child_log" | head -1 | cut -d: -f1) + shell_count=$(grep -ac \ + 'SubAgent \[project-scope-analyst\] tool start .* name=shell_execute' \ + "$child_log") + shell_result_count=$(grep -ac \ + 'SubAgent \[project-scope-analyst\] tool \[shell_execute\] result: Exit code: 0' \ + "$child_log") + status_command_count=$(grep -aEo \ + 'shell_execute#[[:alnum:]-]+\(Command=git status --short, WorkingDirectory=/home/netclaw/\.netclaw/workspaces/project-scope-target,' \ + "$child_log" | wc -l | tr -d ' ') + diff_command_count=$(grep -aEo \ + 'shell_execute#[[:alnum:]-]+\(Command=git diff --stat, WorkingDirectory=/home/netclaw/\.netclaw/workspaces/project-scope-target,' \ + "$child_log" | wc -l | tr -d ' ') + + [[ -n "$declared_line" && -n "$shell_line" && -n "$shell_result_line" \ + && "$shell_count" -eq 2 && "$shell_result_count" -eq 2 \ + && "$status_command_count" -eq 1 && "$diff_command_count" -eq 1 \ + && "$declared_line" -lt "$shell_line" \ + && "$shell_line" -lt "$shell_result_line" ]] +} + setup_coding_context_worktree_handoff() { local run="$1" if (( run % 2 == 1 )); then @@ -2041,6 +2106,9 @@ run_all() { "Use spawn_agent with agent headless-analyst to write a prospecting email to Casey, a VP of Engineering interested in reducing operational toil. Return its final email." \ "Delegate to headless-analyst: draft an outbound email for Jordan, a technology leader evaluating autonomous operations. Return the worker's final email." + run_multi_turn_case subagent_project_scope_declaration "subagent declares a different named project before shell inspection" \ + "Use spawn_agent with agent project-scope-analyst. Ask it to inspect /home/netclaw/.netclaw/workspaces/project-scope-target with exactly two shell_execute calls: git status --short and git diff --stat. Return the command results, project layout, and build-file summary. Keep the parent project unchanged." + PROMPT_TIMEOUT="$previous_timeout" end_category diff --git a/feeds/skills/.system/files/netclaw-operations/SKILL.md b/feeds/skills/.system/files/netclaw-operations/SKILL.md index 73e04d587..9b76ea793 100644 --- a/feeds/skills/.system/files/netclaw-operations/SKILL.md +++ b/feeds/skills/.system/files/netclaw-operations/SKILL.md @@ -3,7 +3,7 @@ name: netclaw-operations description: "REQUIRED when the user asks about scheduling, reminders, cron jobs, timers, background jobs, diagnostics, troubleshooting, MCP tools, daemon health, identity updates, or Netclaw capabilities and self-maintenance." metadata: author: netclaw - version: "2.50.0" + version: "2.51.0" --- # Netclaw Operations @@ -45,10 +45,14 @@ allowed roots); the project's identity file (`.netclaw/AGENTS.md`, `CLAUDE.md`, `skill_read_resource('netclaw-operations', 'references/projects.md')`. Use the `shell_execute` `WorkingDirectory` argument for one command in another -directory. Do not add an inline `cd` unless changing directory is itself the -behavior the user asked you to run or test. Use -`set_working_directory` when later commands and subagents need the same project -root. Do not repeat it when `[working-context]` already names that project. If +directory. This argument and an absolute path operand provide exact scope, but +they do not add a safe-space root. Do not add an inline `cd` unless directory +change is the requested behavior. + +When available, use `set_working_directory` before shell work if several +commands target another user-named project. +This rule also applies to subagents and commands with absolute path operands. +Do not repeat the call when `[working-context]` already names that project. If the tool rejects a path, correct the path and retry it before work continues. For Team and Personal sessions, `[working-context]` is refreshed at the start diff --git a/feeds/skills/.system/files/netclaw-operations/references/projects.md b/feeds/skills/.system/files/netclaw-operations/references/projects.md index f3d8e6978..bf7e70bed 100644 --- a/feeds/skills/.system/files/netclaw-operations/references/projects.md +++ b/feeds/skills/.system/files/netclaw-operations/references/projects.md @@ -16,6 +16,10 @@ Use `set_working_directory` to set or change the project directory: set_working_directory(path: "/workspace/service") ``` +An absolute shell path gives approval policy an exact candidate scope. It does +not add that directory as a safe-space root. When the declaration tool is +available, declare a different user-named project before several shell calls. + Rules: - The path must be an absolute path to an existing directory diff --git a/openspec/changes/structure-shell-approval-policy/design.md b/openspec/changes/structure-shell-approval-policy/design.md index c2a2da7fd..6cc46a6c1 100644 --- a/openspec/changes/structure-shell-approval-policy/design.md +++ b/openspec/changes/structure-shell-approval-policy/design.md @@ -285,6 +285,17 @@ model call. Later child tool calls use the new scope. The child reports the local scope in its result, but the parent merge keeps its existing rule: child project selection does not replace the parent project directory. +Model guidance distinguishes an exact candidate scope from a declared safe +root. An absolute path operand lets policy bind a candidate to that path. It +does not add a safe-space root or make an otherwise uncovered phrase safe. If +a task needs several shell calls in a user-named project that differs from the +current project, the agent declares that project before the first shell call. +This rule also applies to subagents whose exposed tools include the declaration +tool, and to commands with absolute operands. One shell call can use the typed +`WorkingDirectory` argument without changing the persistent project root. The +final headless subagent contract conditionally repeats the multi-command rule +after role guidance so the execution boundary stays clear. + The shared `set_working_directory` validation rejects NUL, CR, and LF before filesystem resolution. This rule applies to both execution and the eligibility probe. An invalid path returns a bounded error and cannot enter model history, diff --git a/openspec/changes/structure-shell-approval-policy/specs/tool-approval-gates/spec.md b/openspec/changes/structure-shell-approval-policy/specs/tool-approval-gates/spec.md index 3f1c5311a..53026b7b2 100644 --- a/openspec/changes/structure-shell-approval-policy/specs/tool-approval-gates/spec.md +++ b/openspec/changes/structure-shell-approval-policy/specs/tool-approval-gates/spec.md @@ -575,6 +575,18 @@ paths SHALL retain existing strict checks. - **WHEN** every earlier stage passes - **THEN** safe policy covers that candidate +#### Scenario: Exact path scope does not declare a safe root + +- **GIVEN** an agent has no declared project root for a user-named project +- **WHEN** a shell candidate contains an absolute path beneath that project +- **THEN** the path can provide the candidate's exact policy scope +- **AND** it does not add that project as a safe-space root +- **AND** model guidance tells the agent to call `set_working_directory` before + several shell calls in that project +- **AND** the same rule applies when a subagent's exposed tools include + `set_working_directory` and its inherited project differs +- **AND** the rule is absent when that tool is unavailable + #### Scenario: Undeclared project scope returns an agent correction - **GIVEN** every shell candidate has a reviewed-safe phrase diff --git a/openspec/changes/structure-shell-approval-policy/tasks.md b/openspec/changes/structure-shell-approval-policy/tasks.md index 8c26da9fe..d270cf6a3 100644 --- a/openspec/changes/structure-shell-approval-policy/tasks.md +++ b/openspec/changes/structure-shell-approval-policy/tasks.md @@ -135,9 +135,12 @@ - [x] 8.3 Keep trace data out of model prompts and session journals. - [x] 8.4 Update consumer and operator guides with complete input, facts, coverage, trace, and output examples. -- [ ] 8.5 Update the `netclaw-operations` skill and deterministic approval evals - for schema 3 and the authored-source boundary. -- [ ] 8.6 Update `IMPLEMENTATION_PLAN.md` and canonical OpenSpec requirements. +- [x] 8.5 Update the always-loaded rules, the `netclaw-operations` skill, and + deterministic approval evals for schema 3 and the authored-source boundary. + Remove the false claim that an absolute path declares a safe root. Add a + subagent eval that proves a different user-named project is declared before + the child's first shell inspection. +- [x] 8.6 Update `IMPLEMENTATION_PLAN.md` and canonical OpenSpec requirements. ## 9. Validation and staged delivery diff --git a/src/Netclaw.Actors.Tests/SubAgents/SubAgentActorTests.cs b/src/Netclaw.Actors.Tests/SubAgents/SubAgentActorTests.cs index 154c11d14..cc33ab553 100644 --- a/src/Netclaw.Actors.Tests/SubAgents/SubAgentActorTests.cs +++ b/src/Netclaw.Actors.Tests/SubAgents/SubAgentActorTests.cs @@ -178,7 +178,8 @@ public void Tool_call_markup_detection_ignores_quoted_examples(string text, bool public async Task System_prompt_includes_headless_subagent_contract() { var fakeClient = new FakeChatClient(); - var definition = CreateDefinition(); + var scopeTool = new FakeNetclawTool(SetWorkingDirectoryTool.ToolName, "ok"); + var definition = CreateDefinition([scopeTool]); var agent = Sys.ActorOf(SubAgentActor.CreateProps(definition, fakeClient, PermissivePolicy())); var result = await agent.Ask( @@ -193,6 +194,58 @@ public async Task System_prompt_includes_headless_subagent_contract() Assert.Contains("safety, security, trust-boundary, approval, and tool-policy rules remain mandatory", fakeClient.LastReceivedMessages[0].Text); Assert.Contains("Do not ask the user clarifying questions", fakeClient.LastReceivedMessages[0].Text); Assert.Contains("Parent-mediated tool approval", fakeClient.LastReceivedMessages[0].Text); + Assert.Contains("call set_working_directory once, even with absolute paths", fakeClient.LastReceivedMessages[0].Text); + } + + [Fact] + public async Task System_prompt_omits_project_declaration_when_scope_tool_is_unavailable() + { + var fakeClient = new FakeChatClient(); + var agent = Sys.ActorOf(SubAgentActor.CreateProps( + CreateDefinition(), + fakeClient, + PermissivePolicy())); + + var result = await agent.Ask( + new RunSubAgent + { + Scope = SubAgentTestScope.Create(), + Task = "Say hello", + Timeout = TimeSpan.FromSeconds(5) + }, + TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); + + Assert.True(result.Success); + Assert.NotNull(fakeClient.LastReceivedMessages); + Assert.DoesNotContain( + "call set_working_directory once", + fakeClient.LastReceivedMessages[0].Text); + } + + [Fact] + public async Task Public_system_prompt_omits_project_declaration_even_when_definition_contains_scope_tool() + { + var fakeClient = new FakeChatClient(); + var scopeTool = new FakeNetclawTool(SetWorkingDirectoryTool.ToolName, "ok"); + var agent = Sys.ActorOf(SubAgentActor.CreateProps( + CreateDefinition([scopeTool]), + fakeClient, + PermissivePolicy())); + + var result = await agent.Ask( + new RunSubAgent + { + Scope = SubAgentTestScope.Create(audience: TrustAudience.Public), + Task = "Say hello", + Timeout = TimeSpan.FromSeconds(5) + }, + TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); + + Assert.True(result.Success); + Assert.NotNull(fakeClient.LastReceivedMessages); + Assert.DoesNotContain( + "call set_working_directory once", + fakeClient.LastReceivedMessages[0].Text); } [Fact] diff --git a/src/Netclaw.Actors/SubAgents/SubAgentActor.cs b/src/Netclaw.Actors/SubAgents/SubAgentActor.cs index edb8e645c..4c4b94916 100644 --- a/src/Netclaw.Actors/SubAgents/SubAgentActor.cs +++ b/src/Netclaw.Actors/SubAgents/SubAgentActor.cs @@ -46,7 +46,7 @@ public sealed class SubAgentActor : ReceiveActor, IWithTimers + "If tools are available and you still need those operations, call the tools using structured tool calls now. " + "Otherwise provide a final answer based only on executed tool results. " + "Do not include , , or markup in final text."; - private const string HeadlessExecutionContract = """ + private const string HeadlessExecutionContractPrefix = """ [Subagent Execution Contract] You are a headless, non-interactive worker running on behalf of a parent Netclaw session. Your subagent role guidance and assigned task are more specific than inherited deployment or project guidance. If they conflict, follow your subagent guidance and assigned task. @@ -56,8 +56,11 @@ [Subagent Execution Contract] If the task is ambiguous, make reasonable assumptions and state them in your final output. If you are blocked, return a final result that explains what you found, what remains, and what decision the parent session needs. Parent-mediated tool approval may occur only for concrete tool calls; it is a security gate, not a dialogue channel. - Always end by emitting a final output for the parent session. """; + private const string ProjectScopeDeclarationContract = + "Before repeated shell work in another task-named project, call set_working_directory once, even with absolute paths."; + private const string HeadlessExecutionContractSuffix = + "Always end by emitting a final output for the parent session."; private static readonly TimeSpan StreamPingInterval = TimeSpan.FromSeconds(2); private readonly SubAgentDefinition _definition; @@ -362,7 +365,10 @@ private void Idle() // system prompt stays reproducible across invocations. _history.Add(new AiChatMessage( Microsoft.Extensions.AI.ChatRole.System, - BuildSystemPrompt(_definition, _projectInstructions))); + BuildSystemPrompt( + _definition, + _projectInstructions, + CanDeclareProjectScope()))); _history.Add(new AiChatMessage(Microsoft.Extensions.AI.ChatRole.User, BuildUserMessage( msg.RuntimeContext, @@ -828,6 +834,12 @@ private IReadOnlyList ResolveExposedAiTools() .ToList(); } + private bool CanDeclareProjectScope() => + _aiTools.Any(tool => string.Equals( + tool.Name, + SetWorkingDirectoryTool.ToolName, + StringComparison.Ordinal)); + private bool _completed; private void Complete( @@ -1062,7 +1074,10 @@ private void TryApplyProjectDirectory(SerializableChatMessage result) nextScope.Audience, projectDirectory); - var prompt = BuildSystemPrompt(_definition, _projectInstructions); + var prompt = BuildSystemPrompt( + _definition, + _projectInstructions, + CanDeclareProjectScope()); var systemMessage = new AiChatMessage(Microsoft.Extensions.AI.ChatRole.System, prompt); if (_history.Count > 0 && _history[0].Role == Microsoft.Extensions.AI.ChatRole.System) _history[0] = systemMessage; @@ -1530,7 +1545,8 @@ private sealed record SubAgentToolCallResult( private static string BuildSystemPrompt( SubAgentDefinition definition, - string? projectInstructions) + string? projectInstructions, + bool canDeclareProjectScope) { // Assemble the identity stack that sub-agents inherit from the parent session: // 1. Embedded operating core + deployment AGENTS.md mission playbook @@ -1546,7 +1562,15 @@ private static string BuildSystemPrompt( // Append the headless execution and precedence contract — always at the bottom, // after the specialized role prompt it protects from inherited mission conflicts. - return string.Concat(rolePrompt.TrimEnd(), "\n\n", HeadlessExecutionContract); + var projectScopeContract = canDeclareProjectScope + ? string.Concat(ProjectScopeDeclarationContract, "\n") + : string.Empty; + return string.Concat( + rolePrompt.TrimEnd(), + "\n\n", + HeadlessExecutionContractPrefix, + projectScopeContract, + HeadlessExecutionContractSuffix); } private sealed class ChildFileActivityTracker diff --git a/src/Netclaw.Configuration.Tests/FileSystemPromptProviderAudienceTests.cs b/src/Netclaw.Configuration.Tests/FileSystemPromptProviderAudienceTests.cs index cbf5e6b03..a283e1b55 100644 --- a/src/Netclaw.Configuration.Tests/FileSystemPromptProviderAudienceTests.cs +++ b/src/Netclaw.Configuration.Tests/FileSystemPromptProviderAudienceTests.cs @@ -77,8 +77,12 @@ public void Personal_rules_prefer_typed_shell_working_directory_and_retry_failed Assert.Contains("`WorkingDirectory` argument", prompt); Assert.Contains("Do not prefix the command with an inline `cd`", prompt); + Assert.Contains("Path arguments give the approval gate an exact candidate scope", prompt); + Assert.Contains("safe-space root", prompt); + Assert.DoesNotContain("path argument IS the declaration", prompt); Assert.Contains("before the first shell", prompt); - Assert.Contains("Do not repeat it when", prompt); + Assert.Contains("Do not repeat", prompt); + Assert.Contains("`project_dir` already names the correct project", prompt); Assert.Contains("changing directory is itself behavior", prompt); Assert.Contains("correct the path and retry the tool", prompt); Assert.Contains("Do not continue with a stale directory", prompt); diff --git a/src/Netclaw.Configuration/Resources/AGENTS.md b/src/Netclaw.Configuration/Resources/AGENTS.md index aa6d7a0ce..0dd02eea1 100644 --- a/src/Netclaw.Configuration/Resources/AGENTS.md +++ b/src/Netclaw.Configuration/Resources/AGENTS.md @@ -19,33 +19,24 @@ ## Declaring Project Scope (load-bearing for approvals) -Path arguments to shell commands declare scope implicitly. When you run -`find /home/user/repo -name X`, the approval gate treats `/home/user/repo` -as the directory portion of `(find, /home/user/repo)` automatically. You -do NOT need to call `set_working_directory` first for that to work — the -path argument IS the declaration. Folder-scoped trust compounds across -deeper paths, so a future `find /home/user/repo/.netclaw` is auto-allowed. - -**When `set_working_directory` IS the right tool**, it's for sessions -where the agent will run multiple commands without explicit path -arguments — typical interactive REPL work, `git status` followed by -`git diff` followed by edits, or `make build` and similar tools that -hide their target behind flags (`make -C`, `git -C`). When the user names -that project, call `set_working_directory ` before the first shell -command. Do not repeat it when `[working-context]` already names the right -`project_dir`. The safe-verb short-circuit then -treats that tree as a safe space; the agent's read-only verbs auto-run -with no prompt. - -When the user task is scoped to a project or codebase the user named -explicitly (a directory path, a repo, "this codebase"), declaring -scope — either by passing the path on each command or by calling -`set_working_directory` once — keeps the approval prompts from -interrupting every read-only inspection. Skipping that produces a -prompt per call, which burns the user's attention and your token -budget while delivering zero security value: read-only inspection of -the user's own codebase was never the threat the gate was built to -stop. +Path arguments give the approval gate an exact candidate scope. They do not +add a safe-space root or make an uncovered command safe. A stored folder +grant can cover deeper paths beneath its approved root. + +Call `set_working_directory ` before the first shell command when all of +these conditions apply: + +- The user or assigned task names a project or codebase. +- `[working-context]` does not name that project as `project_dir`. +- The work needs several shell calls in that project. +- The `set_working_directory` tool is available. + +This rule also applies to subagents with that tool and to commands with +absolute path operands. Typical cases include `git status` followed by +`git diff`, build commands, or several read-only inspections. Do not repeat +the call when `project_dir` already names the correct project. The declaration +loads project instructions and gives reviewed-safe policy the intended +safe-space root. When NOT to declare scope at all: pure-conversation turns ("what's 2+2?", "explain X"), sessions where no project has been mentioned, or @@ -54,7 +45,8 @@ one-shot lookups against external APIs. Calling own kind of noise. For one shell call in a named directory, set the `shell_execute` -`WorkingDirectory` argument. Do not prefix the command with an inline `cd`. +`WorkingDirectory` argument. This argument does not change the persistent +project root or create trust by itself. Do not prefix the command with an inline `cd`. Inline `cd` changes control flow. In `cd && A; B`, command `B` can run after a failed `cd`, so approval analysis cannot use the requested directory. Keep inline `cd` only when changing directory is itself behavior that the user