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 to exfiltrate your data. It just doesn’t know what’s sensitive. Ask it to “find configuration files” and it might helpfully include ~/.aws/credentials in its search. Ask it to “clean up temporary files” and who knows what it considers temporary.
Running arbitrary AI-generated code requires trust you probably shouldn’t extend.
The Sandbox That Already Exists
Deno denies all permissions by default. A script 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)
If a script tries something it wasn’t permitted to do, Deno blocks it at runtime: not a warning, not a prompt, but a hard stop.
This is useful. If AI generates a script that accidentally reaches for something it shouldn’t, Deno catches it. The script fails instead of succeeding at something you didn’t want.
Two Files, One Trust Boundary
When AI generates a tool, ask it to produce two files:
- A wrapper script (bash or PowerShell) that humans read and approve
- A Deno script that does the actual work
The wrapper is the trust boundary. It’s short, simple, and lists exactly what permissions it grants. Users read the wrapper before running it. The Deno script can be as complex as needed 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 that lists permissions, one deno run command, no logic.
A user can read this in thirty seconds and understand exactly what it does. That’s the point.
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 denies matter. --deny-net documents that network access was considered and rejected, not just forgotten.
What the Deno Script Should Do
The Deno 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);
If 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 approach handles accidental overreach, not adversarial scripts. If AI deliberately wants to do something malicious, it can do malicious things within the permissions you grant. A script with --allow-write="./output" could write something harmful to that directory.
You’re trusting that:
- The AI isn’t actively hostile
- The wrapper accurately describes what it does
- You actually read the wrapper before running it
The second point matters. If you skip reading the wrapper and just execute it, you’re back to trusting arbitrary AI-generated code. The safety comes from the human checkpoint, not magic.
The Subprocess Escape Hatch
--allow-run breaks the model.
If you grant --allow-run=npm, the Deno script can spawn npm. That npm process runs outside the sandbox with full system access. A compromised package with a postinstall script can do anything. Deno’s permissions don’t cascade to child processes.
This matters because supply chain attacks are real. You’re not just trusting the AI-generated code; you’re trusting every transitive dependency that code might install or invoke.
The fix: avoid --allow-run when possible. For npm packages specifically, Deno can import them directly:
import express from "npm:express@4.18.2";
Modules imported this way run inside Deno’s sandbox. They’re subject to the same permission checks as your code. No subprocess, no escape hatch.
If your script genuinely needs to spawn external processes, the sandbox approach described here isn’t sufficient. You’d need OS-level isolation (containers, VMs) or to accept that you’re back to trusting the code.
The Point
Deno’s permission model exists. It’s a real sandbox, enforced at runtime, not a gentleman’s agreement. Using it for AI-generated scripts gives you an actual trust boundary instead of a prayer.
Read the wrapper, run the wrapper, and let Deno enforce the limits.
The alternative is trusting that AI knows what’s sensitive. It doesn’t.
Template
A CLAUDE.md file to guide AI toward generating safer scripts:
# Deno Script Generation
When generating scripts, produce two files:
1. **Wrapper script** (bash) - the trust 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 must list 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)