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
capabilitiesfield incodemod.yamlprovides 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
Unlikefetch 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 default
readFileSync, 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
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 Nodeerr.code strings so existing catch logic keeps working:
| Code | Meaning |
|---|---|
EACCES | Path is outside target_dir. |
ENOENT | Path is inside target_dir but the file doesn’t exist. |
EIO | Backing storage failure (disk error, remote fetch failed). |
EINVAL | Path 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 viacapabilities:
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:Core utilities (14 modules)
Core utilities (14 modules)
assert- Assertion testingbuffer- Binary data manipulationconsole- Logging and debuggingcrypto- Cryptographic operationsevents- Event emitter patternsfs(sandboxed totarget_dir)os- Operating system information (read-only)path- File path utilitiesperf_hooks- Performance measurementprocess- Process information (read-only, no spawning)string_decoder- String encoding/decodingtimers- Timer functionstty- TTY operationsutil- Utility functions
Web standards (3 modules)
Web standards (3 modules)
stream_web- Web Streams APIurl- URL parsing and manipulationzlib- 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 upgradefs from sandboxed to unrestricted), add a capabilities field to your codemod.yaml file:
codemod.yaml
Capability names
Use these exact strings in thecapabilities array:
| Capability | What it changes | Use Case |
|---|---|---|
fs | Swaps the default sandboxed fs for LLRT’s unrestricted real-disk fs. | Reading files outside target_dir (home config, caches). |
fetch | Exposes the fetch global. | Making HTTP requests to APIs. |
child_process | Exposes the child_process module. | Running Git, npm, or other CLI tools. |
Usage examples
Default (sandboxed) file system
Nocapabilities entry needed — this just works:
scripts/codemod.ts
Unrestricted file system
Network requests
Running external commands
CLI override flags
You can also enable capabilities via CLI flags when running a codemod: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
Prefer the default sandboxed fs
Prefer the default sandboxed fs
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.Minimize required capabilities
Minimize required capabilities
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.
Document why capabilities are needed
Document why capabilities are needed
In your README, explain why each capability is required and what operations use it.
README.md
Validate inputs when using child_process
Validate inputs when using child_process
Never pass unsanitized user input to shell commands:
Consider publishing capability-free versions
Consider publishing capability-free versions
If possible, provide a version of your codemod that works without unsafe capabilities for maximum trust.
Security considerations
Next steps
Intro
Build your first JSSG codemod
Advanced patterns
Learn advanced transformation techniques