Skip to content

Bug: Non-interactive channels always fail with shell_no_trust_zone_roots #1244

@petabridge-netclaw

Description

@petabridge-netclaw

Bug: Non-interactive channels always fail with shell_no_trust_zone_roots

Description

All non-interactive channel types — Headless (netclaw chat -p), Reminder, and Webhook — fail to execute any shell commands with shell_no_trust_zone_roots, regardless of verb pre-approval. This makes shell execution impossible in automated contexts.

The problem has two independent root causes that combine to make this unavoidable:

1. Trust zone enforcement runs before the approval gate

In ToolAccessPolicy.AuthorizeInvocation, the trust zone check executes before CheckApprovalGate. Pre-approved verbs never reach the matcher:

// Line 156-166: Trust zone enforcement — runs first
if (context?.SupportsInteractiveApproval == false && shellCommand is not null)
{
    if (_shellTrustZonePolicy is null)
    {
        if (ShellCommandHasTrustZoneSensitiveInputs(shellCommand, workingDirectory))
            return ToolAccessDecision.Deny("shell_trust_zone_policy_not_configured");
    }
    else
    {
        var trustZoneDeny = EnforceShellTrustZones(shellCommand, workingDirectory, context);
        if (trustZoneDeny is not null)
            return trustZoneDeny;  // ← blocks here
    }
}

// Line 171: Approval gate — never reached
return CheckApprovalGate(toolName, context, arguments, ShellApprovalMatcher.Instance);

2. Personal audience resolves to zero trust zone roots

ShellTrustZonePolicy.GetTrustZoneRoots() calls ScopedFileAccessPolicy.GetRootsForContext(context, AccessKind.Write). For the Personal profile, WriteFiles.Mode is "All", which causes ResolveRoots to short-circuit:

// ToolAudienceProfileResolver.cs:34-35
if (access.Mode != ToolFilesystemMode.Roots)
    return [];  // ← Personal has Mode: "All" → returns empty

Then EnforceShellTrustZones immediately denies on the empty set:

// ToolAccessPolicy.cs:183-184
var roots = _shellTrustZonePolicy!.GetTrustZoneRoots(context);
if (roots.Count == 0)
    return ToolAccessDecision.Deny("shell_no_trust_zone_roots");  // ← hits here

Affected Channels

All channels where SupportsInteractiveApproval == false:

Channel Use Case Blocked?
Headless (netclaw chat -p) User at terminal, single prompt
Reminder Automated tasks with pre-approved verbs
Webhook Inbound event processing

Reproduction

# 1. Pre-approve a verb
netclaw approvals trust-verb netclaw

# 2. Run in headless mode
echo "Run 'netclaw stats'" | netclaw chat -p -
# Result: shell_no_trust_zone_roots

# 3. Create a reminder with channel delivery
set_reminder(id="test", prompt="Run 'netclaw stats'", schedule="2m",
             deliveryKind="channel", deliveryTransport="slack", deliveryAddress="#channel")
# Result: shell_no_trust_zone_roots

Source References (commit 76ea1c3)

Proposed Fix

Option A — Return meaningful roots for Personal audience

ShellTrustZonePolicy should return sensible defaults for the Personal audience even when WriteFiles.Mode == "All":

// Pseudocode
if (audience == TrustAudience.Personal)
{
    roots = [session_dir, workspaces_dir, identity_dir];
}

Option B — Check pre-approval before trust zone enforcement

If the verb is globally pre-approved, skip trust zone path validation. Pre-approval is the explicit authorization mechanism for unattended execution.

Option C — Distinguish automated channel types

Not all non-interactive channels have the same threat model:

  • Webhooks — external payloads, stay strict
  • Reminders — user-created, inherit creator's pre-approved verbs
  • Headless — user at terminal, deserves interactive-level trust

This would require per-channel-type trust zone configuration rather than a binary SupportsInteractiveApproval check.

Questions for Discussion

  1. Should pre-approved verbs bypass trust zone path validation, or should trust zones always apply?
  2. What are the correct trust zone roots for Personal audience when WriteFiles.Mode == "All"?
  3. Should Headless, Reminder, and Webhook share the same security posture, or should they be distinguished?

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingsecuritySecurity-related changes

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions