AI can write useful scripts. It can also write scripts that read your SSH keys and POST them somewhere interesting.

The problem isn’t malice. AI doesn’t want your secrets. It just can’t tell what’s sensitive. Ask it to “find configuration files” and it might helpfully include ~/.aws/credentials. Ask it to “clean up temporary files” and, well, its definition of temporary is broader than yours.

Don’t run arbitrary AI-generated code.


The Sandbox That Already Exists

Deno denies all permissions by default. Scripts can’t read files, write files, access the network, spawn processes, or read environment variables unless you explicitly allow it.

deno run script.ts                           # Can do almost nothing
deno run --allow-read=./input script.ts      # Can read one directory
deno run --allow-all script.ts               # Can do anything (don't do this)

A script attempting something unpermitted gets blocked at runtime. Hard stop. No dialog box asking if you’re sure.

So if AI generates a script that reaches for something it shouldn’t, Deno catches it before anything happens.


Two Files, One Trust Boundary

Ask AI to produce two files:

  1. A wrapper script (bash or PowerShell) that humans read and approve
  2. A Deno script that does the actual work

The wrapper is the trust boundary. It’s short, simple, lists exactly what permissions it grants. A user reads it before running it. The Deno script can be complex because it runs inside the wrapper’s constraints.

user reads wrapper → user runs wrapper → wrapper invokes Deno with limited permissions → Deno script runs constrained

The wrapper shouldn’t be clever. Clever is hard to verify.


What a Good Wrapper Looks Like

#!/bin/bash
# convert-csv.sh - Convert CSV to JSON

# === Variables ===
INPUT_FILE="$1"
OUTPUT_FILE="$2"
SCRIPT="scripts/convert-csv.ts"

# === Help ===
if [ -z "$INPUT_FILE" ] || [ "$1" = "--help" ]; then
  echo "Usage: ./convert-csv.sh <input.csv> <output.json>"
  echo ""
  echo "Converts CSV file to JSON format."
  echo ""
  echo "Permissions granted:"
  echo "  - Read: input file"
  echo "  - Write: output file"
  echo "  - Denied: network, env, subprocess"
  exit 1
fi

# === Run ===
deno run \
  --allow-read="$INPUT_FILE" \
  --allow-write="$OUTPUT_FILE" \
  --deny-net --deny-env --deny-run \
  "$SCRIPT" "$INPUT_FILE" "$OUTPUT_FILE"

Variables at the top. Help text listing permissions. One deno run command, no logic. A user reads it in thirty seconds and understands exactly what it does.


Permission Patterns

File processing, no network:

--allow-read="$INPUT" --allow-write="$OUTPUT" --deny-net --deny-env --deny-run

Calls Ollama locally:

--allow-read="$INPUT" --allow-write="$OUTPUT" --allow-net="localhost:11434" --deny-env --deny-run

Explicit --deny is important. It documents that a capability was considered and deliberately refused, not just forgotten. There’s a difference between “we didn’t grant network access” and “we thought about network access and said no.”


What the Deno Script Should Do

The script receives file paths as command line arguments and operates only on those paths. No hardcoded paths that bypass the wrapper. No reaching for things the wrapper didn’t grant.

const inputPath = Deno.args[0];
const outputPath = Deno.args[1];

const data = await Deno.readTextFile(inputPath);
// ... process data ...
await Deno.writeTextFile(outputPath, result);

The script tries to read something else? Deno blocks it. The wrapper’s permissions are enforced at runtime, not by convention.


What This Doesn’t Solve

This handles accidental overreach, not adversarial scripts. If AI deliberately wants to do something malicious within the permissions you grant, it can. A script with --allow-write="./output" could write something harmful there.

You’re trusting:

  1. The AI isn’t actively hostile
  2. The wrapper accurately describes what it does
  3. You actually read the wrapper before running it

Third point is the one people skip. If you don’t read the wrapper before running it, you’ve collapsed the entire trust model into “I trust this AI” which is where we started.


The Subprocess Escape Hatch

--allow-run breaks the model.

Grant --allow-run=npm and the Deno script can spawn npm. That process runs outside the sandbox with full system access. A compromised package with a postinstall script exploits this. Deno’s permissions don’t cascade to children.

Avoid --allow-run when possible. For npm packages, Deno can import them directly:

import express from "npm:express@4.18.2";

Modules imported this way run inside Deno’s sandbox. Same permission checks as your code. No subprocess escape hatch.

If your script needs to spawn external processes, this sandbox approach isn’t sufficient. Accept that you’re trusting the code, or use OS-level isolation (containers, VMs).


The Point

Deno’s permission model isn’t theoretical. It’s enforced at runtime, on every syscall. Using it for AI-generated scripts means you have an actual trust boundary, not a gentleman’s agreement that the script will behave.

Read the wrapper. Run the wrapper. Let Deno do the rest.

The other option is trusting that AI understands what’s sensitive on your machine. It does not.


Template for CLAUDE.md

# Deno Script Generation

When generating scripts, produce two files:

1. **Wrapper script** (bash) — the boundary users read before running
2. **Deno script** (TypeScript) — the actual logic

## Wrapper Requirements

- All variables at the top (parameters AND config like API hosts)
- Help text when called without args or with `--help`
- Help lists what permissions are granted
- Single `deno run` command with explicit permissions
- No logic beyond argument parsing

## Wrapper Template

```bash
#!/bin/bash
# tool-name.sh - Brief description

# === Variables ===
INPUT_FILE="$1"
OUTPUT_FILE="$2"
SCRIPT="scripts/tool-name.ts"

# === Help ===
if [ -z "$INPUT_FILE" ] || [ "$1" = "--help" ]; then
  echo "Usage: ./tool-name.sh <input> <output>"
  echo ""
  echo "What this tool does."
  echo ""
  echo "Permissions granted:"
  echo "  - Read: input file"
  echo "  - Write: output file"
  echo "  - Denied: network, env, subprocess"
  exit 1
fi

# === Run ===
deno run \
  --allow-read="$INPUT_FILE" \
  --allow-write="$OUTPUT_FILE" \
  --deny-net --deny-env --deny-run \
  "$SCRIPT" "$INPUT_FILE" "$OUTPUT_FILE"
```

## Permission Patterns

File processing (no network):
`--allow-read="$INPUT" --allow-write="$OUTPUT" --deny-net --deny-env --deny-run`

With local Ollama:
`--allow-read="$INPUT" --allow-write="$OUTPUT" --allow-net="localhost:11434" --deny-env --deny-run`

## Deno Script Requirements

- Accept file paths as command line arguments
- Read only from provided input paths
- Write only to provided output path
- No hardcoded paths
- Import npm packages with `npm:` specifier (runs inside sandbox)

## Never

- Use `--allow-all`
- Use `--allow-run` (breaks sandbox; subprocesses escape permissions)
- Hardcode paths that bypass wrapper permissions
- Shell out to npm/node (use `npm:` imports instead)