Deno Runtime skill for Claude Code
Scoped permissions instead of -A everywhere. Claude sets up Deno projects so every task runs with the narrowest permissions that work: scoped hosts, paths, env vars and binaries recorded as permission sets in deno.json, plus deny rules for metadata endpoints. Dependencies come from JSR and npm with a committed lockfile, tests keep sanitizers on, and shipping goes through deno compile, a pinned container, or Deno Deploy.
- name
deno-runtime - size215 lines · ≈2,200 tokens
- files1
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Builds and runs Deno 2 projects with least-privilege permissions - scoped allow and deny flags, permission sets in deno.json, deno.json tasks and import maps, npm and JSR packages, Node compatibility settings, Deno.serve and deno serve, Deno.test with steps and sanitizers, deno compile, JSR publishing, and Deno Deploy. Use when starting or migrating a project to Deno, replacing -A with scoped permissions, adding npm packages to a Deno app, writing Deno tests, or deploying to Deno Deploy. Try asking
- Replace -A in our deno.json tasks with the minimum permissions each needs
- Port this small Node script to Deno 2 and keep using the npm packages
- Our Deno tests fail with leaking resources errors. Fix them properly
Use it when
- Starting a Deno 2 project or migrating a Node script to Deno
- Tightening permissions on tasks that run with -A
- Adding npm or JSR dependencies to a Deno app
- Writing Deno tests, compiling binaries, or deploying to Deno Deploy
Skip it when
- The project runs on Bun or uses Bun-specific APIs — use bun-runtime
- The target is Cloudflare Workers bindings and limits — use cloudflare-workers
How to check the result
deno install --frozen, deno check, lint, fmt --check and deno test -P pass, no task contains -A, and removing one permission makes the app fail with NotCapable.
The common mistake it prevents
Granting --allow-run or unscoped --allow-env to make one call work, which quietly gives the process the same power as -A.
Done well means: no task runs with -A, each permission is scoped to the paths, hosts,
env vars and binaries actually used, dependencies are pinned in deno.json with a
committed deno.lock, and deno check, deno lint, deno fmt --check and deno test
all pass in CI.
Workflow
- [ ] 1. Inventory the Deno version, config and dependency sources
- [ ] 2. Define tasks and imports in deno.json; commit deno.lock
- [ ] 3. Derive least-privilege permissions and encode them in config
- [ ] 4. Write the server or script on web-standard APIs
- [ ] 5. Test with Deno.test, sanitizers on, permissions scoped
- [ ] 6. Ship via deno compile, a container, or Deno Deploy
1. Inventory
deno --version
cat deno.json deno.jsonc package.json 2>/dev/null
deno info main.ts | head -40 # dependency graph: jsr:, npm:, https:, local
grep -rn "deno.land/std" --include=*.ts . | head # legacy std imports to migrate
Record: Deno 2 minor (2.5 added permission sets in config, 2.6 more granular permissions
and deno audit, 2.7 stabilized the Temporal API, 2.9 shipped June 2026), whether the
project is package.json-driven or deno.json-driven, and any https:// imports. Migrate
deno.land/std URLs to jsr:@std/* when touching those files.
2. deno.json
{
"tasks": {
"dev": "deno run --watch -P main.ts",
"start": "deno run -P main.ts",
"test": "deno test -P",
"check": "deno check . && deno lint && deno fmt --check"
},
"imports": {
"@std/assert": "jsr:@std/assert@^1.0.0",
"@std/http": "jsr:@std/http@^1.0.0",
"zod": "npm:zod@^4.0.0"
},
"permissions": {
"default": {
"net": ["0.0.0.0:8000", "api.stripe.com:443", "db.internal:5432"],
"env": ["DATABASE_URL", "STRIPE_KEY", "PORT"],
"read": ["./static"]
}
},
"test": {
"permissions": { "read": ["./fixtures"], "env": ["DATABASE_URL"] }
},
"lint": { "rules": { "tags": ["recommended"] } },
"fmt": { "lineWidth": 100 }
}
deno add jsr:@std/path npm:pg # writes to imports with a caret range
deno remove npm:pg
deno install # install everything (also reads package.json)
deno outdated # then update ranges deliberately
deno install --frozen # CI: fail if deno.lock would change
-Pwith no value applies thedefaultpermission set;-P=nameor--permission-set=nameapplies a named one (Deno 2.5+). Oncetest.permissionsexists,deno testmust be run with-Por explicit flags.- On Deno older than 2.5, write the scoped flags into each task string instead.
nodeModulesDir: leave unset (global cache) for pure Deno apps; set"auto"when a framework or tool expects a realnode_modules(Vite, Next-style tooling, packages that resolve files from disk),"manual"when another package manager owns it.- npm lifecycle scripts do not run by default; allow specific ones with
deno install --allow-scripts=npm:<pkg>only after reviewing what they do.
3. Least-privilege permissions
| Need | Flag (scoped) | Avoid |
|---|---|---|
| Listen on a port | --allow-net=0.0.0.0:8000 | --allow-net |
| Call specific APIs | --allow-net=api.example.com:443 | wildcard hosts |
| Read config/static files | --allow-read=./static,./config | --allow-read on / |
| Write output | --allow-write=./out | --allow-write |
| Env vars | --allow-env=DATABASE_URL,PORT | --allow-env |
| Spawn a binary | --allow-run=git | --allow-run (equals full access) |
| FFI / native addons | --allow-ffi=./lib/libfoo.so | blanket --allow-ffi |
| Block something inside a broad allow | --deny-net=169.254.169.254 | relying on review alone |
Derivation loop:
- Run with no permissions in a terminal; Deno prompts for each access. Note every
request, or set
DENO_AUDIT_PERMISSIONS=./perm-audit.jsonl(2.5+) to log checks. - Encode the minimal set in
deno.jsonpermissions or task flags. - Add
--no-prompt(non-interactive contexts already fail instead of prompting) so a missing permission fails loudly rather than hanging. - Treat
--allow-run,--allow-ffi, unscoped--allow-writeand--allow-envas equivalent to-A; justify each in a comment or the PR. - Deny cloud metadata endpoints (
169.254.169.254) when net access is broad.
4. Server code
// main.ts
import { z } from "zod";
const Order = z.object({ sku: z.string().min(1), qty: z.number().int().min(1).max(99) });
export default {
async fetch(req: Request): Promise<Response> {
const url = new URL(req.url);
if (req.method === "GET" && url.pathname === "/healthz") return new Response("ok");
if (req.method === "POST" && url.pathname === "/orders") {
const parsed = Order.safeParse(await req.json().catch(() => null));
if (!parsed.success) return Response.json(z.flattenError(parsed.error), { status: 422 });
return Response.json({ id: crypto.randomUUID(), ...parsed.data }, { status: 201 });
}
return new Response("not found", { status: 404 });
},
} satisfies Deno.ServeDefaultExport;
deno serve -P --port 8000 main.ts # uses the default export
deno serve -P --parallel main.ts # one worker per core
Or call Deno.serve({ port: 8000, signal }, handler) directly; pass an AbortSignal
and abort it on SIGTERM (Deno.addSignalListener("SIGTERM", ...)) to drain cleanly.
Hono, Oak and Fresh run on Deno when a framework is wanted.
Node compatibility: node: built-ins and most npm packages work. CommonJS loads from
.cjs files or packages with "type": "commonjs"; require is not available in ESM
.ts files (use createRequire from node:module if needed). Globals like process
and Buffer exist in npm packages; in your own code import them from node:process
and node:buffer for clarity.
5. Tests
import { assertEquals, assertRejects } from "@std/assert";
import handler from "./main.ts";
Deno.test("POST /orders validates qty", async (t) => {
await t.step("rejects qty 0", async () => {
const res = await handler.fetch(new Request("http://x/orders", {
method: "POST",
body: JSON.stringify({ sku: "A1", qty: 0 }),
}));
assertEquals(res.status, 422);
await res.body?.cancel(); // unconsumed bodies trip the resource sanitizer
});
});
deno test -P --coverage=cov
deno coverage cov --lcov > cov.lcov
deno test -P --filter "orders" --fail-fast
- Sanitizers fail tests that leak timers, open files, or unconsumed response bodies.
Fix the leak; disable
sanitizeOps/sanitizeResourcesper test only for a documented third-party leak. @std/testingprovidesbdd(describe/it),mock(stub,spy) andtime(FakeTime). Deno 2.5+ also hasDeno.test.beforeAll/beforeEach/afterAll/afterEachhooks.- Tests get only the permissions you pass; a test that needs the network must say so.
6. Ship
| Target | Command / approach |
|---|---|
| Single binary | deno compile -P --output app main.ts; cross-compile with --target x86_64-unknown-linux-gnu. Permissions are baked in at compile time |
| Container | FROM denoland/deno:<exact-version>, RUN deno install --frozen, run deno run -P main.ts as the deno user |
| Deno Deploy (GA since Feb 2026) | Connect the GitHub repo or use the deploy CLI described in current Deploy docs; set env vars in the dashboard; no local filesystem writes |
| JSR package | deno publish --dry-run, then deno publish; requires name, version, exports in deno.json |
Deploy tooling changed during 2025 (the classic deployctl flow was replaced by the new
Deploy platform). Check the current docs before writing CI steps instead of copying older
examples.
Gotchas
deno.lockmust be committed; without it, caret ranges float between machines.--allow-netwithout a port allows every port on that host.- Remote
https://imports need--allow-importfor hosts outside Deno’s default allowlist; prefer JSR or npm specifiers over raw URLs. deno checkis separate fromdeno run; type errors do not stopdeno rununless--checkis passed.Deno.openKv()and other unstable APIs need the matching--unstable-*flag or"unstable": ["kv"]in deno.json.
Verification
deno install --frozen
deno task check # deno check . && deno lint && deno fmt --check
deno test -P
grep -n '"-A"\|--allow-all\| -A ' deno.json # must return nothing
Run the production command once with a permission removed (e.g. drop one env var from the
set) and confirm it fails with a clear NotCapable error rather than misbehaving; that
proves the permission boundary is real.
About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.