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.

  • Backend & APIs
  • $deno-apps
  • 178 lines
  • 2 files
  • ~1,750 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- deno-apps

Installs to ~/.agents/skills/deno-apps/ for every project on this machine.

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

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.

~/.agents/skills/deno-apps/SKILL.md

--- name: deno-apps description: Builds and runs Deno 2 applications - deno.json imports with jsr and npm specifiers, nodeModulesDir and package.json compatibility, least-privilege… license: MIT ---

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

  1. Read AGENTS.md, deno.json / deno.jsonc (imports, tasks, permissions, nodeModulesDir, compilerOptions), any package.json, and deno.lock.
  2. deno --version. These instructions assume Deno 2.x; Deno 1 projects need the migration notes below first.
  3. 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 cached DENO_DIR or 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:zod updates imports for you; deno outdated lists upgrades.
  • Prefer JSR for packages published there (typed, ESM, provenance); use npm: otherwise.
  • "lock": { "frozen": true } (or --frozen) makes CI fail if deno.lock would change.
  • Named permission sets need Deno 2.5 or later; on older 2.x, write the flags in each task.

Permissions

FlagScope it toExample
--allow-netHost and port--allow-net=0.0.0.0:8000,api.example.com
--allow-read / --allow-writePaths--allow-read=./static --allow-write=./tmp
--allow-envVariable names--allow-env=DATABASE_URL,PORT
--allow-runExecutables--allow-run=git (grants a lot; spawned processes are not sandboxed)
--allow-ffi, --allow-sysAvoid unless requiredFFI escapes the sandbox entirely
--deny-*Carve exceptions--allow-read --deny-read=/etc

Workflow to tighten an -A app:

  1. Run the test suite and the app without -A. Deno prompts or throws NotCapable / PermissionDenied naming the exact resource.
  2. Add each needed resource to a permission set, scoped as narrowly as the table shows.
  3. Re-run until clean, then set DENO_NO_PROMPT=1 in CI so missing permissions fail instead of waiting for input.
  4. 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 real node_modules (bundlers, Prisma, packages that read their own files at runtime). "manual" means you run deno install or npm install yourself.
  • npm lifecycle scripts do not run by default. Allow specific packages: deno install --allow-scripts=npm:sharp.
  • CommonJS loads from .cjs files or packages marked "type": "commonjs"; require in an ESM .js file fails.
  • Frameworks: Next.js, Astro, SvelteKit, Express and Fastify run with nodeModulesDir set; 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/x URL imports still work, but move to JSR or npm via imports.
  • Removed APIs (Deno.run, Deno.readAll, several Deno.* stream helpers) need Deno.Command and Web streams; deno lint flags many of them.
  • window is gone; use globalThis.

Checklist

  • No -A in tasks used for production; permission sets scoped
  • Dependencies in imports; deno.lock committed and frozen in CI
  • deno fmt --check, deno lint, deno check ., deno test -P pass
  • SIGTERM handled; deploy target is the current Deno Deploy

Gotchas

  • --allow-net without a value grants every host, including cloud metadata endpoints.
  • --allow-run plus --allow-write effectively grants everything; treat it as -A.
  • Deno.openKv() locally needs --unstable-kv; check KV availability on your Deploy plan before building on it.
  • deno serve expects a default export; a file that calls Deno.serve itself must be run with deno run.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

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.

esc
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem