Skip to main content
JSSG follows a deny-by-default security model for the most sensitive operations (network requests and spawning processes) and a sandboxed-by-default model for file system access. By default, codemods can work only with files under the target directory; anything else that could touch the outside world is either scoped down or gated behind an explicit capability.

Why defense in depth?

Codemods run arbitrary code transformations on your codebase. By restricting or scoping capabilities, JSSG ensures:
  • Safe execution: Untrusted codemods can’t exfiltrate secrets, read files outside the project, reach the network, or run shell commands without explicit permission.
  • Audit trail: The capabilities field in codemod.yaml provides a clear record of what extra permissions a codemod requires.
  • Principle of least privilege: The default surface is the minimum a typical transformation needs — just the files under your target directory.

File system — sandboxed by default

Unlike fetch and child_process, the fs module is always available but is constrained to the workflow’s target directory (the folder you’re running the codemod against). Reads and writes inside that directory succeed transparently; paths that normalize outside it are rejected with a Node-style EACCES error.

Inside target_dir

Allowed by defaultreadFileSync, writeFileSync, readdirSync, mkdirSync, statSync, existsSync, unlinkSync, and their fs/promises equivalents all work.

Outside target_dir

Rejected with EACCESAbsolute paths outside the target or .. traversals that escape it throw a Node-style error with err.code === "EACCES".
scripts/codemod.ts
import type { Codemod } from "codemod:ast-grep";
import { readFileSync } from "fs";

const codemod: Codemod = (root) => {
  // ✅ Works — adjacent file inside target_dir
  const pkg = JSON.parse(readFileSync("./package.json", "utf-8"));

  try {
    // ❌ Throws EACCES — outside the sandbox
    readFileSync("/etc/passwd", "utf-8");
  } catch (e) {
    // e.code === "EACCES"
  }
};

export default codemod;
Path comparisons first normalize ./.. segments and collapse redundant slashes before the prefix check. When the curated fs is backed by real disk (the CLI), the resolver additionally walks each path component with symlink_metadata and rejects any path that traverses a symlink — a pre-existing target_dir/link-to-elsewhere/secret won’t slip past the guard even though its lexical form stays under target_dir.

Error codes

The curated fs surfaces standard Node err.code strings so existing catch logic keeps working:
CodeMeaning
EACCESPath is outside target_dir.
ENOENTPath is inside target_dir but the file doesn’t exist.
EIOBacking storage failure (disk error, remote fetch failed).
EINVALPath couldn’t be resolved.

Want unrestricted disk access?

If a codemod legitimately needs to read or write outside the target directory (caches, home-directory config, scratch paths in /tmp), opt in explicitly — see Unsafe capabilities below. That swaps the curated fs for the unrestricted LLRT fs module and the path guard is no longer applied.

Unsafe capabilities — require explicit permission

These still require opt-in via capabilities:

fs (unrestricted)

Full real-disk accessUpgrades from the sandboxed curated fs to LLRT’s real-disk fs module. No target_dir prefix check. Use only when the codemod genuinely needs paths outside the target.

fetch

Network requestsMake HTTP/HTTPS requests. Fully deny-by-default.

child_process

Process spawningExecute external commands. Fully deny-by-default.

Default (safe) modules

These modules are always available and require no special permissions:
  • assert - Assertion testing
  • buffer - Binary data manipulation
  • console - Logging and debugging
  • crypto - Cryptographic operations
  • events - Event emitter patterns
  • fs (sandboxed to target_dir)
  • os - Operating system information (read-only)
  • path - File path utilities
  • perf_hooks - Performance measurement
  • process - Process information (read-only, no spawning)
  • string_decoder - String encoding/decoding
  • timers - Timer functions
  • tty - TTY operations
  • util - Utility functions
  • stream_web - Web Streams API
  • url - URL parsing and manipulation
  • zlib - Compression/decompression
Default modules don’t perform network requests or spawn processes. fs is in this list because its default form is sandboxed to target_dir; adding fs to capabilities upgrades it to the unrestricted variant that can read and write anywhere on disk.

Enabling capabilities

To enable unsafe capabilities (or to upgrade fs from sandboxed to unrestricted), add a capabilities field to your codemod.yaml file:
codemod.yaml
name: my-codemod
version: 1.0.0
capabilities:
  - fs             # Upgrade to unrestricted, real-disk fs (disables the target_dir guard)
  - fetch          # Enable network requests
  - child_process  # Enable spawning processes
Only request capabilities your codemod actually needs. Adding fs here removes the target_dir guard — the codemod can now read and write anywhere on disk. If you only need files in the repo, don’t list it.

Capability names

Use these exact strings in the capabilities array:
CapabilityWhat it changesUse Case
fsSwaps the default sandboxed fs for LLRT’s unrestricted real-disk fs.Reading files outside target_dir (home config, caches).
fetchExposes the fetch global.Making HTTP requests to APIs.
child_processExposes the child_process module.Running Git, npm, or other CLI tools.

Usage examples

Default (sandboxed) file system

No capabilities entry needed — this just works:
scripts/codemod.ts
import type { Codemod } from "codemod:ast-grep";
import { readFileSync, writeFileSync } from "fs";

const codemod: Codemod = (root) => {
  // Read an adjacent config file inside the target directory
  const config = JSON.parse(readFileSync("./config.json", "utf-8"));

  // Write a generated artifact next to the source
  writeFileSync("./generated.ts", "export const X = 1;\n", "utf-8");
};

export default codemod;

Unrestricted file system

1

Enable the fs capability (real-disk)

codemod.yaml
capabilities:
  - fs
2

Use fs anywhere on disk

scripts/codemod.ts
import type { Codemod } from "codemod:ast-grep";
import { readFileSync } from "fs";

const codemod: Codemod = (root) => {
  // With `capabilities: [fs]` the target_dir guard is gone — this works.
  const globalConfig = JSON.parse(
    readFileSync("/Users/me/.config/my-tool/config.json", "utf-8")
  );
  // ... use globalConfig in transformation
};

export default codemod;
Listing fs gives the codemod read/write access to every path the host user has permission for. Review untrusted codemods carefully before enabling it.

Network requests

1

Enable fetch capability

codemod.yaml
capabilities:
  - fetch
2

Use fetch in your transform

scripts/codemod.ts
import type { Codemod } from "codemod:ast-grep";

const codemod: Codemod = async (root) => {
  // Fetch latest API definitions
  const response = await fetch("https://api.example.com/schema.json");
  const schema = await response.json();

  // ... use schema in transformation
};

export default codemod;
Note the async transform function when using fetch or other asynchronous operations.

Running external commands

1

Enable child_process capability

codemod.yaml
capabilities:
  - child_process
2

Spawn processes in your transform

scripts/codemod.ts
import type { Codemod } from "codemod:ast-grep";
import { execSync } from "child_process";

const codemod: Codemod = (root) => {
  // Get current git branch
  const branch = execSync("git rev-parse --abbrev-ref HEAD", {
    encoding: "utf-8"
  }).trim();

  // ... use branch info in transformation
};

export default codemod;
Always validate and sanitize any user input before passing it to execSync or spawn to prevent command injection vulnerabilities.

CLI override flags

You can also enable capabilities via CLI flags when running a codemod:
# Upgrade fs to unrestricted (otherwise the default sandboxed fs is used)
codemod jssg run --allow-fs path/to/codemod

# Enable network + unrestricted fs
codemod jssg run --allow-fetch --allow-fs path/to/codemod

# Enable all capabilities (use with caution!)
codemod jssg run --allow-fs --allow-fetch --allow-child-process path/to/codemod

# Or with a workflow
codemod workflow run --allow-fs --allow-fetch --allow-child-process -w path/to/workflow.yaml --target path/to/repo
CLI flags take precedence over codemod.yaml. This is useful for testing or one-off runs where you trust the codemod source. --allow-fs swaps the sandboxed fs for the unrestricted variant — if your codemod only needs files inside the target directory, you don’t need to pass it.

Best practices

If your codemod only reads/writes files inside the repo, don’t add fs to capabilities. The default sandboxed fs already works and keeps the scope of your codemod tight for reviewers.
Only request capabilities your codemod truly needs. For example, if you only read config from the repo, you don’t need any capabilities at all.
In your README, explain why each capability is required and what operations use it.
README.md
## Required Capabilities

- **fs** (unrestricted): Reads `~/.config/my-tool/config.json` outside the repo
- **fetch**: Calls an external API to get data
Never pass unsanitized user input to shell commands:
// ❌ BAD: Command injection vulnerability
const userInput = options.branch;
execSync(`git checkout ${userInput}`);

// ✅ GOOD: Validate input first
const userInput = options.branch;
if (!/^[a-zA-Z0-9_-]+$/.test(userInput)) {
  throw new Error("Invalid branch name");
}
execSync(`git checkout ${userInput}`);
If possible, provide a version of your codemod that works without unsafe capabilities for maximum trust.

Security considerations

Untrusted codemods: Never run codemods from untrusted sources with fs (unrestricted), fetch, or child_process enabled without reviewing the code first. The default sandboxed fs is designed so that even a malicious codemod can’t exfiltrate data outside the target directory.

Next steps

Intro

Build your first JSSG codemod

Advanced patterns

Learn advanced transformation techniques