Skip to content

fix(agent): decide scoped rm in the shared bash fence - #1337

Merged
gewenyu99 merged 8 commits into
mainfrom
posthog/fix-1326-scoped-rm-log
Oct 7, 2026
Merged

gewenyu99 merged 8 commits into
mainfrom
posthog/fix-1326-scoped-rm-log

Conversation

@gewenyu99

@gewenyu99 gewenyu99 commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Dedupe rm tool fencing.

[pi] → bash {"command":"rm -f .posthog-audit-checks.json"}
Denying bash command (not in allowlist): rm -f .posthog-audit-checks.json
[pi] ← bash: {"content":[{"type":"text","text":"(no output)"}]}

Changes

  • Move the scoped rm rule into the shared bash fence, so every pi path makes one policy call.
  • Allowed deletes stop logging and capturing as denied.
  • The .env guard covers every path pi opens.
%%{init: {"block": {"padding": 20}}}%%
block-beta
  columns 11
  callBand["pi callers   old"]:11
  callers["linear run · orchestrator task · subagent"]:7 space:4
  piBand["src/agent/runner/harness/pi   changed"]:11
  gate["evaluateToolCall(piToolPath)"]:7 space:4
  policyBand["src/agent shared policy   old, fence changed"]:11
  policy["wizardCanUseTool: disallowedTools, .env, Grep glob"]:7 space:4
  fence["evaluateBashCommand: scoped rm decided first"]:7 space:1 deny["deny: block, log bash denied"]:3
  allow["allow: YARA scan, then run"]:7 space:4

  callers --> gate
  gate --> policy
  policy --> fence
  fence --> deny
  fence --> allow

  classDef old fill:#9ca3af1f,stroke:#9ca3af,stroke-width:1.5px
  classDef new fill:#3b82f626,stroke:#3b82f6,stroke-width:2px
  classDef oldBand fill:#9ca3af40,stroke:none
  classDef newBand fill:#3b82f640,stroke:none
  class callers,policy,allow,deny old
  class gate,fence new
  class callBand,policyBand oldBand
  class piBand newBand
Loading

Every pi call, from a linear run, an orchestrator task or a subagent, goes through one gate and one policy call. Blue is what changed: the gate passes the tool path, and the bash fence decides a scoped rm before anything logs a deny. Deny is the only branch that logs and captures bash denied.

These guards are in effect on pi. The anthropic arm pre-allows Bash, Read, Write, Edit and Grep, and its SDK sandbox scopes writes to the project. bash denied stays inside wizardCanUseTool, since every deny it returns is now enforced.

Test plan

Tests lock the rm bypasses, the pi @ and file:// paths, the Grep glob, and the subagent gate. A real pi audit ran the rm step and logged Allowing bash command.

pnpm typecheck, pnpm lint (0 errors) and pnpm test (200 files, 3393 tests) pass.

LLM context

Written by Claude Code after a review of the first version of this PR.

Created with PostHog Desktop

On pi, a scoped `rm` of a project file (for example the audit skills'
`rm -f .posthog-audit-checks.json`) ran, but the log said "Denying bash
command (not in allowlist)" and analytics captured `bash denied`.
evaluateToolCall asked wizardCanUseTool first, which logs and captures
every allowlist deny, and only then applied the scoped-rm rule.

The scoped-rm rule is now decided first. A scoped rm logs an allow line
and skips wizardCanUseTool, but still blocks when the program disallows
Bash. Every other call goes through wizardCanUseTool as before.

Part of #1326

Generated-By: PostHog Desktop
Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589
@github-actions

Copy link
Copy Markdown

🧙 Wizard CI

Run the Wizard CI and test your changes against wizard-workbench example apps by replying with a GitHub comment using one of the following commands:

Test all apps:

  • /wizard-ci all

Test all apps in a directory:

  • /wizard-ci ai-observability
  • /wizard-ci basic-integration
  • /wizard-ci feature-flags
  • /wizard-ci mcp-analytics
  • /wizard-ci replay-vision
  • /wizard-ci revenue
  • /wizard-ci self-driving
  • /wizard-ci warehouse
  • /wizard-ci warehouse-seeded

Test an individual app:

  • /wizard-ci ai-observability/anthropic
  • /wizard-ci ai-observability/google-adk
  • /wizard-ci ai-observability/groq
Show more apps
  • /wizard-ci ai-observability/manual-capture
  • /wizard-ci ai-observability/openai
  • /wizard-ci ai-observability/openai-agents
  • /wizard-ci ai-observability/opentelemetry
  • /wizard-ci ai-observability/vercel-ai
  • /wizard-ci basic-integration/android
  • /wizard-ci basic-integration/angular
  • /wizard-ci basic-integration/astro
  • /wizard-ci basic-integration/django
  • /wizard-ci basic-integration/fastapi
  • /wizard-ci basic-integration/flask
  • /wizard-ci basic-integration/flutter
  • /wizard-ci basic-integration/javascript-node
  • /wizard-ci basic-integration/javascript-web
  • /wizard-ci basic-integration/laravel
  • /wizard-ci basic-integration/next-js
  • /wizard-ci basic-integration/nuxt
  • /wizard-ci basic-integration/python
  • /wizard-ci basic-integration/rails
  • /wizard-ci basic-integration/react-native
  • /wizard-ci basic-integration/react-router
  • /wizard-ci basic-integration/sveltekit
  • /wizard-ci basic-integration/swift
  • /wizard-ci basic-integration/tanstack-router
  • /wizard-ci basic-integration/tanstack-start
  • /wizard-ci basic-integration/vue
  • /wizard-ci feature-flags/django
  • /wizard-ci feature-flags/next-js
  • /wizard-ci mcp-analytics/custom-dispatcher
  • /wizard-ci mcp-analytics/typescript-sdk
  • /wizard-ci replay-vision/javascript-node
  • /wizard-ci replay-vision/next-js
  • /wizard-ci replay-vision/react-vite
  • /wizard-ci revenue/stripe
  • /wizard-ci self-driving/astro
  • /wizard-ci self-driving/fastapi
  • /wizard-ci self-driving/nuxt
  • /wizard-ci self-driving/react-router
  • /wizard-ci self-driving/sveltekit
  • /wizard-ci warehouse/monorepo-env
  • /wizard-ci warehouse/multi-source-next
  • /wizard-ci warehouse/stripe-node
  • /wizard-ci warehouse/zero-source
  • /wizard-ci warehouse-seeded/next-stripe
  • /wizard-ci warehouse-seeded/next-stripe-declined

Test against a Context Mill branch:

  • /wizard-ci all context-mill:my-branch

Add context-mill:<branch> to any command above to pin the Context Mill branch. It defaults to main.

Results will be posted here when complete.

AGENTS.md keeps new code comments to one line.

Part of #1326

Generated-By: PostHog Desktop
Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589
The scoped-rm rule lived in pi's gate as an exception beside the shared
bash fence. wizardCanUseTool denied the command, logged it and captured
`bash denied`, and then pi ran it anyway. The exception also skipped every
other check in wizardCanUseTool, never reached orchestrator task runs, and
let subagents delete files.

The rule now lives in evaluateBashCommand, which takes a project root, and
wizardCanUseTool passes workingDirectory through. evaluateToolCall makes one
policy call, so an allowed rm logs an allow line and captures nothing.

- Containment compares real paths, refuses `..`, and refuses quotes,
  escapes, globs, redirects, and any whitespace but a space.
- createSecurityExtension requires workingDirectory, so task runs get the
  rule. Subagents get subagentFactory, the same fence and state with no rm,
  branded so the parent's gate doesn't type-check there.
- The .env guard ignores case, checks pi paths after pi strips `@` and
  decodes file://, and denies a Grep glob that can select a .env file.
  minimatch matches those globs with dotfile semantics like ripgrep.
- A denied rm states the rule, or says rm isn't available without a root.

Part of #1326

Generated-By: PostHog Desktop
Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589
The Windows and POSIX containment tests exercise isScopedFileRemoval, which
now lives in bash-fence.ts, so they move from the pi security tests to
bash-fence-rm.test.ts. Also names the real-path target, reads a pi tool
path once, and states the rm rule in the fence header without comparing it
to another harness. No behavior change.

Part of #1326

Generated-By: PostHog Desktop
Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589
@gewenyu99 gewenyu99 changed the title fix(pi): decide a scoped rm before the allowlist logs it as denied fix(agent): decide scoped rm in the shared bash fence (WIP) Sep 24, 2026
@gewenyu99
gewenyu99 marked this pull request as ready for review September 24, 2026 22:38
@gewenyu99
gewenyu99 requested a review from a team as a code owner September 24, 2026 22:38
@gewenyu99 gewenyu99 changed the title fix(agent): decide scoped rm in the shared bash fence (WIP) fix(agent): decide scoped rm in the shared bash fence Sep 24, 2026
Comment thread src/shared/utils/env-scan.ts Outdated
@veria-ai

veria-ai Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 1 · PR risk: 0/10

@johncwaters johncwaters left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved by hand after checking the automated review.

Note

Automated review. Not written by a human.

Comment thread src/shared/utils/env-scan.ts Outdated
return isEnvFileName(name.toLowerCase());
}

const ENV_FILE_SAMPLES = ['.env', '.env.local', 'app/.env', 'app/.env.local'];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Automated review. Not written by a human.

globCanSelectEnvFile only tests the glob against these four sample names, so a glob that selects any other env file passes: .env.production, .env.prod*, *.production, .env.development.local, .envrc. Since a ripgrep --glob overrides .gitignore and the hidden-file skip, a Grep with glob .env.production still searches production secrets.

Fix: decide on the glob itself rather than a sample list (for example, deny when the glob's last segment could match a name starting with .env, any case), or at least widen the samples to the common suffixes plus .envrc, with tests for .env.production and *.production.

Comment thread src/shared/utils/env-scan.ts Outdated
/** True when a ripgrep `--glob` can select a `.env` file. Such a glob overrides `.gitignore`, and ripgrep's `*` matches dotfiles. */
export function globCanSelectEnvFile(glob: string): boolean {
const options = { dot: true, nocase: true, matchBase: true };
return ENV_FILE_SAMPLES.some((name) => minimatch(name, glob, options));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Automated review. Not written by a human.

Nit: minimatch reads a ripgrep exclude glob like !*.min.js as a negation that matches .env, so Grep calls that only narrow the search get denied. An exclude never adds files, so a leading ! can be allowed; worth a test.

gewenyu99 and others added 4 commits October 7, 2026 09:59
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	src/agent/runner/harness/pi/security.ts
A leading-wildcard glob such as *.foo was tested only against .env, .envrc and
stage names, so it could still select .env.foo. Check it against the project's
real env files under the Grep path as well, keeping the name-list and
literal-prefix rules.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@gewenyu99
gewenyu99 merged commit 666c5c4 into main Oct 7, 2026
18 checks passed
@gewenyu99
gewenyu99 deleted the posthog/fix-1326-scoped-rm-log branch October 7, 2026 14:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants