Deno 2 Apps skill for Codex
Scoped permissions instead of -A everywhere. Codex sets up Deno 2 projects with dependencies in deno.json, a frozen lockfile and least-privilege permission sets it discovers by running without -A and reading each denial. It handles npm packages that need node_modules or install scripts, keeps fmt, lint, check and test in one task, and treats deployment as a confirmed step on the current Deno Deploy rather than the retired Classic platform and deployctl.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- deno-apps Installs to ~/.agents/skills/deno-apps/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install deno-apps The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project deno-apps Installs to .agents/skills/deno-apps/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/deno-apps.zipunzip -o deno-apps.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) deno-apps PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $deno-apps, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Builds and runs Deno 2 applications - deno.json imports with jsr and npm specifiers, nodeModulesDir and package.json compatibility, least-privilege permission flags and named permission sets, Deno.serve and deno serve, deno test with @std/assert, deno fmt, lint and check, deno compile, and deploying to the current Deno Deploy with the deno deploy command. Use when starting or migrating a Deno project, fixing PermissionDenied or npm import errors, tightening -A to scoped permissions, or moving apps off Deploy Classic and deployctl.
Use it when
- Starting a new Deno 2 service, CLI or library
- PermissionDenied, NotCapable or npm import errors appear under Deno
- Upgrading a Deno 1 codebase or replacing deno.land/x imports
- Deploying to Deno Deploy or compiling a standalone binary
Use something else when
- The project runs on Bun: use bun-runtime
- The target is Cloudflare's runtime rather than Deno: use cloudflare-workers
How to check the result
deno fmt --check, deno lint, deno check . and deno test -P pass, and the app starts with its named permission set and no permission prompts under DENO_NO_PROMPT=1.
The mistake it prevents
Shipping tasks with -A or a bare --allow-net, which removes the sandbox that is Deno's main security advantage.
Done well means: the app runs with the narrowest permissions that work (never -A in
production), dependencies are declared in deno.json and locked in deno.lock, type
checking, formatting and linting run in CI, and the deploy target is the current Deno
Deploy, not the retired Classic platform.
Before changing anything
- Read
AGENTS.md,deno.json/deno.jsonc(imports, tasks, permissions,nodeModulesDir,compilerOptions), anypackage.json, anddeno.lock. deno --version. These instructions assume Deno 2.x; Deno 1 projects need the migration notes below first.- Baseline:
deno task(lists tasks),deno check .,deno lint,deno test -P. First runs download modules and need network; in an offline sandbox, use the cachedDENO_DIRor ask before allowing network.
Configuration
{
"tasks": {
"dev": "deno serve --watch -P=server main.ts",
"start": "deno serve -P=server main.ts",
"check": "deno fmt --check && deno lint && deno check . && deno test -P"
},
"imports": {
"@std/assert": "jsr:@std/assert@^1",
"@std/http": "jsr:@std/http@^1",
"hono": "jsr:@hono/hono@^4",
"zod": "npm:zod@^3"
},
"permissions": {
"server": {
"net": ["0.0.0.0:8000", "api.stripe.com:443"],
"env": ["DATABASE_URL", "STRIPE_KEY", "PORT"],
"read": ["./static"]
}
},
"test": { "permissions": { "read": ["./fixtures"], "env": true } },
"lock": { "frozen": true }
}
deno add jsr:@std/assert npm:zodupdatesimportsfor you;deno outdatedlists upgrades.- Prefer JSR for packages published there (typed, ESM, provenance); use
npm:otherwise. "lock": { "frozen": true }(or--frozen) makes CI fail ifdeno.lockwould change.- Named permission sets need Deno 2.5 or later; on older 2.x, write the flags in each task.
Permissions
| Flag | Scope it to | Example |
|---|---|---|
--allow-net | Host and port | --allow-net=0.0.0.0:8000,api.example.com |
--allow-read / --allow-write | Paths | --allow-read=./static --allow-write=./tmp |
--allow-env | Variable names | --allow-env=DATABASE_URL,PORT |
--allow-run | Executables | --allow-run=git (grants a lot; spawned processes are not sandboxed) |
--allow-ffi, --allow-sys | Avoid unless required | FFI escapes the sandbox entirely |
--deny-* | Carve exceptions | --allow-read --deny-read=/etc |
Workflow to tighten an -A app:
- Run the test suite and the app without
-A. Deno prompts or throwsNotCapable/PermissionDeniednaming the exact resource. - Add each needed resource to a permission set, scoped as narrowly as the table shows.
- Re-run until clean, then set
DENO_NO_PROMPT=1in CI so missing permissions fail instead of waiting for input. - Remember npm dependencies run under the same permissions: an SDK that reads unrelated env vars will need them listed or will fail at startup.
Serving HTTP
// main.ts, run with: deno serve -P=server main.ts
export default {
async fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/healthz") return new Response(null, { status: 204 });
if (req.method === "POST" && url.pathname === "/api/orders") {
const body = await req.json();
return Response.json(await createOrder(body), { status: 201 });
}
return Response.json({ error: "not found" }, { status: 404 });
},
} satisfies Deno.ServeDefaultExport;
Or programmatically, with graceful shutdown:
const ac = new AbortController();
const server = Deno.serve({ port: Number(Deno.env.get("PORT") ?? 8000), signal: ac.signal }, handler);
Deno.addSignalListener("SIGTERM", () => ac.abort());
await server.finished;
deno serve --parallel runs one worker per CPU core for the default-export form. For
routing beyond a handful of paths, use Hono from JSR or @std/http route.
Node and npm compatibility
- Import built-ins with the
node:prefix (import fs from "node:fs/promises"). - With a
package.json, Deno reads its dependencies; set"nodeModulesDir": "auto"when a tool expects a realnode_modules(bundlers, Prisma, packages that read their own files at runtime)."manual"means you rundeno installornpm installyourself. - npm lifecycle scripts do not run by default. Allow specific packages:
deno install --allow-scripts=npm:sharp. - CommonJS loads from
.cjsfiles or packages marked"type": "commonjs";requirein an ESM.jsfile fails. - Frameworks: Next.js, Astro, SvelteKit, Express and Fastify run with
nodeModulesDirset; verify with the project’s own test suite rather than assuming.
Testing and quality
import { assertEquals, assertRejects } from "@std/assert";
Deno.test("createOrder rejects zero quantity", async () => {
await assertRejects(() => createOrder({ sku: "A1", quantity: 0 }), RangeError);
});
Deno.test("total sums lines", async (t) => {
await t.step("empty", () => assertEquals(total([]), 0));
await t.step("two lines", () => assertEquals(total([{ q: 2, p: 5 }, { q: 1, p: 3 }]), 13));
});
deno test -P # uses "test.permissions" from deno.json
deno test --coverage=cov && deno coverage cov
deno fmt --check && deno lint && deno check .
deno run and deno serve do not type-check by default; keep deno check . in CI.
Compile and deploy
deno compile -P=server --include ./static -o dist/app main.ts
Permissions are baked into the compiled binary; review them before distributing.
Deno Deploy: Deploy Classic (dash.deno.com) and deployctl are retired in favour of the
current platform at console.deno.com and the deno deploy subcommand, with the GitHub
integration as the usual path. Classic projects do not migrate automatically; they are
recreated as new apps. Deploying is a production action: confirm the target organization,
app and environment with the user before running any deploy command, and set secrets in
the dashboard per context (production, development, build) rather than in code.
Migrating from Deno 1
deno.land/xURL imports still work, but move to JSR or npm viaimports.- Removed APIs (
Deno.run,Deno.readAll, severalDeno.*stream helpers) needDeno.Commandand Web streams;deno lintflags many of them. windowis gone; useglobalThis.
Checklist
- No
-Ain tasks used for production; permission sets scoped - Dependencies in
imports;deno.lockcommitted and frozen in CI -
deno fmt --check,deno lint,deno check .,deno test -Ppass - SIGTERM handled; deploy target is the current Deno Deploy
Gotchas
--allow-netwithout a value grants every host, including cloud metadata endpoints.--allow-runplus--allow-writeeffectively grants everything; treat it as-A.Deno.openKv()locally needs--unstable-kv; check KV availability on your Deploy plan before building on it.deno serveexpects a default export; a file that callsDeno.serveitself must be run withdeno run.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Deno 2 Apps"
short_description: "Scoped permissions instead of -A everywhere"
default_prompt: "$deno-apps Replace -A in our Deno tasks with the narrowest permissions that still work" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.