Agent Tool Allowlists: Least Privilege for Filesystem and Shell Access

Agent Tool Allowlists: Least Privilege for Filesystem and Shell Access

Coding agents with unrestricted shell access are remote unattended engineers with root-adjacent habits. Default-deny filesystem and command allowlists turn “agent helpfulness” into something you can defend in a security review.

⚡ TL;DR: Allowlist write paths (src/**, tests/**) and commands (pnpm test, tsc, git status). Deny git push --force, npm publish, curl|bash, cloud destructive APIs, and reads of .env. Elevation requires a human token. Implement as a policy check before every tool call — see Claude Code Hooks and LLM Coding Agents on AWS: Safe Tool Sandboxes.

Policy object evaluated on every tool call

export type ToolRequest =
  | { type: "read"; path: string }
  | { type: "write"; path: string }
  | { type: "shell"; argv: string[] };

const WRITE_ALLOW = [/^src\//, /^tests\//, /^packages\/[^/]+\/src\//];
const READ_DENY = [/^\.env/, /^\.aws\//, /id_rsa$/, /credentials\.json$/];
const SHELL_ALLOW: { bin: string; args?: RegExp }[] = [
  { bin: "pnpm", args: /^(test|lint|typecheck|exec tsc)\b/ },
  { bin: "git", args: /^(status|diff|log|add|commit|checkout -b)\b/ },
  { bin: "rg", args: /.*/ },
];

const SHELL_DENY = [
  /push\s+--force/,
  /\bpublish\b/,
  /curl\s+.*\|\s*(ba)?sh/,
  /kubectl\s+delete/,
  /aws\s+iam\s+/,
  /rm\s+-rf\s+\//,
];

export function authorize(req: ToolRequest): { allow: boolean; reason: string } {
  if (req.type === "read") {
    if (READ_DENY.some((re) => re.test(req.path))) {
      return { allow: false, reason: `read_denied:${req.path}` };
    }
    return { allow: true, reason: "ok" };
  }
  if (req.type === "write") {
    if (!WRITE_ALLOW.some((re) => re.test(req.path))) {
      return { allow: false, reason: `write_not_allowlisted:${req.path}` };
    }
    return { allow: true, reason: "ok" };
  }
  const line = req.argv.join(" ");
  if (SHELL_DENY.some((re) => re.test(line))) {
    return { allow: false, reason: `shell_denied:${line}` };
  }
  const bin = req.argv[0];
  const rest = req.argv.slice(1).join(" ");
  const ok = SHELL_ALLOW.some((r) => r.bin === bin && (r.args?.test(rest) ?? true));
  return ok ? { allow: true, reason: "ok" } : { allow: false, reason: `shell_not_allowlisted:${line}` };
}

Elevation path for rare exceptions

async function runTool(req: ToolRequest, ctx: { elevationToken?: string }) {
  const decision = authorize(req);
  if (decision.allow) return execute(req);
  if (ctx.elevationToken && await verifyElevation(ctx.elevationToken, req)) {
    audit.elevated(req);
    return execute(req);
  }
  throw new Error(`policy_denied:${decision.reason}`);
}

✅ Elevation is scoped to one argv fingerprint and expires in minutes.
❌ A sticky “YOLO mode” for the whole session.

Filesystem jail in sandboxes

When tools run remotely, combine allowlists with the sandbox patterns from LLM Coding Agents on AWS: ephemeral compute, no long-lived credentials, egress allowlists. Local IDE agents still need the same argv policy — hooks as in Claude Code Hooks.

Suggested default denials

Category Examples
Git push --force, git filter-branch, rewrite of main
Packages npm publish, twine upload
Cloud aws iam, terraform apply, kubectl delete
Supply chain curl \| bash, unpinned npx from HTTP
Secrets read .env*, *.pem, ~/.aws

Closing checklist

  • [ ] Default-deny shell; explicit allowlist of bins + arg patterns
  • [ ] Write allowlist limited to source/test trees
  • [ ] Secret paths denied on read
  • [ ] Elevation is single-use, fingerprint-scoped, audited
  • [ ] CI policy unit tests for deny cases (force-push, publish, curl|sh)
  • [ ] Remote sandboxes add egress + credential constraints

Related reading

Last updated on September 11, 2026


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

1 Comment

Leave a Reply