Reference

Security Model

Validated structure, quoted path substitutions, a documented recipe-shell contract, an auditable network policy for fetch(), an access policy for env(), and hard budgets on manifest evaluation and file reads. A Netsukefile deserves the same care as a Makefile: Netsuke reduces common quoting and exhaustion mistakes, but it is not a sandbox.

Network Policy

The fetch() standard-library function downloads content from URLs at template evaluation time. Because this happens before a build runs, Netsuke provides configurable network-policy controls so that teams can audit and restrict what remote resources a manifest is allowed to contact.

--fetch-allow-scheme <SCHEME>

Allow additional URL schemes beyond the defaults. Repeat the flag to allow multiple schemes.

--fetch-allow-host <HOST>

Allow the provided hostname when default-deny mode is active. Wildcards such as *.example.com are supported.

--fetch-block-host <HOST>

Always block the provided hostname, even if it appears on the allowlist. Wildcards are supported.

--fetch-default-deny

Deny all hosts by default. Only hosts explicitly allowed by --fetch-allow-host may be contacted.

--trust-project-fetch-policy Since v0.1.0-beta4

Allow project configuration to widen fetch-policy grants. The same opt-in is trust_project_fetch_policy = true in system or user configuration, or NETSUKE_TRUST_PROJECT_FETCH_POLICY=true. It is read only from operator-controlled layers, so no project file can authorize itself.

Project configuration only narrows

Breaking in v0.1.0-beta4

Fetch policy does not follow ordinary configuration precedence. System and user configuration, NETSUKE_ variables, and CLI flags are operator policy; the primary project .netsuke.toml is an untrusted project request.

  • A project fetch_default_deny = true turns default-deny on; false cannot turn an operator's default-deny off.
  • Project fetch_block_host entries accumulate with every other layer, and a block always beats an allow.
  • Project fetch_allow_scheme and fetch_allow_host entries are ignored unless the operator has opted in. With the opt-in they are appended to the operator's own.
  • Files loaded through extends are not quarantined and keep ordinary file-layer precedence.

Every redirect is rechecked

Since v0.1.0-beta4

fetch() applies the policy to the URL it was given and to every redirect destination before opening a connection. An HTTPS-to-HTTP redirect succeeds only when http is explicitly allowed and the destination host passes too.

  • 301, 302, 303, 307, and 308 are followed with GET semantics; relative Location values resolve against the current URL.
  • Chains stop after five redirects. A repeated destination, a missing or invalid Location, and a refused destination each fail with their own localized diagnostic.
  • URL credentials are dropped when a redirect changes origin, and redirect diagnostics never print userinfo.
  • With cache=true the entry is keyed on the original URL; a miss rechecks every hop before storing the body, and a hit makes no request.
Restricting fetch to a single host
$ netsuke \
  --fetch-default-deny \
  --fetch-allow-host releases.example.com \
  --fetch-block-host internal.example.com \
  build

Manifest environment access

Since v0.1.0-beta4

env() reads process environment variables while a manifest renders. Two lists of exact variable names decide which it may read. With neither list configured, env() stays default-allow, so existing manifests behave as before.

Configuration file

env_allow_var = ["CI", "PACKAGE_REGISTRY_TOKEN"]
env_block_var = ["AWS_SECRET_ACCESS_KEY"]

Environment (JSON arrays)

NETSUKE_ENV_ALLOW_VAR='["CI"]'
NETSUKE_ENV_BLOCK_VAR='["GITHUB_TOKEN"]'

CLI (one name, repeatable)

$ netsuke --env-allow-var CI \
    --env-block-var GITHUB_TOKEN build

How the lists combine

  • One effective env_allow_var entry makes the allowlist restrictive: only listed names resolve.
  • An env_block_var list on its own blocks only its listed names. A block always overrides an allow.
  • Every entry is one exact name. There are no globs or patterns.
  • env_allow_var entries in the primary project .netsuke.toml are removed before the policy is composed, so a checkout cannot grant itself access or switch on default-deny. Project env_block_var entries still count, because they can only restrict. Files loaded through extends keep ordinary precedence.
  • A refused lookup fails with “Access to an environment variable is blocked.” The netsuke_manifest_env_lookups_total counter records each lookup by outcome (success, blocked, not_present, not_unicode) and never the name.

The policy reduces how many secrets reach rendered manifest fields. It does not make rendered manifests safe to log; an allowed value still needs the usual output discipline. Full detail is in the upstream users' guide.

Resource budgets

Since v0.1.0-beta4

Netsuke bounds every manifest evaluation before it runs a build command or answers help targets. Each limit is a configuration key with a matching NETSUKE_ variable (the key in upper case) and a --manifest-* flag.

Key and flag Default Bounds
manifest_evaluation_fuel
--manifest-evaluation-fuel
NETSUKE_MANIFEST_EVALUATION_FUEL
1,000,000 MiniJinja instructions for one evaluation
manifest_fuel
--manifest-fuel
NETSUKE_MANIFEST_FUEL
100,000,000 MiniJinja instructions for the whole manifest
manifest_rendered_value_bytes
--manifest-rendered-value-bytes
NETSUKE_MANIFEST_RENDERED_VALUE_BYTES
1 MiB Bytes emitted by one rendered value
manifest_rendered_manifest_bytes
--manifest-rendered-manifest-bytes
NETSUKE_MANIFEST_RENDERED_MANIFEST_BYTES
16 MiB Bytes emitted across all rendered values
manifest_source_bytes
--manifest-source-bytes
NETSUKE_MANIFEST_SOURCE_BYTES
4 MiB Template and macro-import source consumed
manifest_foreach_cardinality
--manifest-foreach-cardinality
NETSUKE_MANIFEST_FOREACH_CARDINALITY
10,000 Values consumed from one foreach iterator
manifest_expanded_entries
--manifest-expanded-entries
NETSUKE_MANIFEST_EXPANDED_ENTRIES
50,000 Targets and actions emitted by expansion

Who may change a limit

Set budgets in trusted configuration, the environment, or on the command line. A project .netsuke.toml, including its extends chain, may lower a ceiling but never raise one set by the defaults, user configuration, the environment, or the CLI. Zero, negative, or unparsable values are configuration errors, not silently ignored.

What a breach looks like

The failure reads Manifest resource budget exhausted with the stage and the numeric limit — never template text, context values, or rendered secrets. In JSON output its code is netsuke::runner::manifest_budget_exceeded.

Budgets reduce exhaustion exposure; they do not remove it. MiniJinja materializes a macro result before its size can be checked, so a macro can allocate past the per-value ceiling before the limit is reported. ADR-018 records the gap. Separately, MiniJinja 2.24.0 refuses a single repeated string above 100 MB with repeated string is too large.

Bounded file reads

Breaking in v0.1.0-beta4

The contents, linecount, hash, and digest filters share one read policy, so a checkout entry cannot exhaust Netsuke's memory or wedge a render.

8 MiB per read

The default budget matches the fetch() response limit. A call may pass max_bytes to lower it; a larger value is clamped to the ceiling. An oversized file fails with a diagnostic naming the path and the limit, never the contents. There is no configuration key or flag for the ceiling in v0.1.0-beta4; programs embedding Netsuke set it with StdlibConfig::with_file_max_read_bytes.

No final symlink

A symbolic link as the last path component is refused; a symlinked directory earlier in the path is fine. On Windows every reparse point is refused: junctions, mount points, and placeholder tags too. follow_symlinks=true waives only that check. A link whose target is written as an absolute path is still refused, because the workspace capability tests the link target, not where it resolves.

Regular files only

A FIFO or device node is refused once opened; on Unix the open is non-blocking, so a FIFO cannot stall the render first. contents and linecount also require UTF-8; hash and digest accept any bytes.

vars:
  version: "{{ 'generated/version.txt' | contents(follow_symlinks=true) }}"
  header_sum: "{{ 'fixtures/header.bin' | hash(max_bytes=1024) }}"

Recipe shells

Recipes are shell strings in v0.1.x. Which interpreter runs them is a fixed contract per platform, not a property of the shell that launched netsuke. Structured executable arguments (RFC 0001) are planned for v0.2.0 and do not change the contract below.

Unix: /bin/sh -e

Each command list entry runs in its own brace group, joined with &&, and is passed to eval as a shell-quoted payload so a trailing # or & cannot break the chain. Groups run in the current shell, so a cd or variable assignment carries to later entries. An entry may start at most one background job; structured or nested exec forms are rejected during Ninja generation.

Windows: PowerShell by default

Every legacy recipe is handed to Windows PowerShell (powershell.exe, not pwsh) as an encoded, non-interactive, no-profile command. A command list shares one process and is checked with $LASTEXITCODE after every entry. Use PowerShell syntax: $name is a variable and $env:NAME reads the environment. {{ ins }} and {{ outs }} render as single-quoted PowerShell arguments. Recipes over Windows' command-line limit and up to 1 MiB are routed through a self-deleting .ps1 response file.

Windows: explicit Bash route

Set NETSUKE_WINDOWS_SHELL=bash to keep POSIX interpretation through Git Bash or MSYS2 (bash.exe on PATH). Netsuke verifies bash.exe --version before build or Ninja-tool execution and stops with install instructions otherwise. In CI, install Git explicitly, prepend its bin directory to PATH, and launch Netsuke from a pwsh step rather than relying on a workflow-wide shell: bash.

Keeping POSIX recipes on Windows
choco install git --yes --no-progress
$env:PATH = "C:\Program Files\Git\bin;$env:PATH"
$env:NETSUKE_WINDOWS_SHELL = "bash"
netsuke build

Markers and shell dollars

{{ ins }} and {{ outs }} are the only Netsuke markers for input and output paths, and the only values that are automatically shell-quoted. Everything else a template renders is ordinary command text. Since v0.1.0-beta3, shell dollar expressions are written normally: Netsuke lowers the markers first and only then escapes literal dollars as $$ in the generated Ninja file, so the shell receives them unchanged. Since v0.1.0-beta4 that holds in script: recipes too: $in, $out, $ins, and $outs are ordinary shell variables in both command: and script:.

Write this

targets:
  - name: build/app
    sources: src/main.c
    command: >-
      ${CC:-cc} $CFLAGS {{ ins }} -o {{ outs }}
      && echo "built on $HOSTNAME"

$CC, $CFLAGS, and $HOSTNAME reach /bin/sh as literal shell variables. On the PowerShell route, write $env:CC instead; PowerShell does not perform POSIX ${VAR:-default} expansion.

Migrating a beta2 or beta3 manifest

  • Replace the historical spelling $$PATH with $PATH. On POSIX and Bash routes $$ is the shell's process identifier; in PowerShell it is the last token of the previous command, so the extra dollar now changes the result.
  • Replace any $in or $out path placeholder with {{ ins }} or {{ outs }}. Beta3 still rewrote them in script: recipes; since v0.1.0-beta4 they reach the shell as ordinary variables there too, as $ins and $outs always have.
  • On POSIX and Bash routes a marker may appear unquoted, single-quoted, or double-quoted, but not inside a command substitution or backticks. In PowerShell, use markers unquoted; quoted and command-substitution sites are rejected.
  • Build and default-target paths escape a space as $ , Ninja's own path escape. They reject | and control characters, which Ninja cannot represent, and also $ and colons, which Ninja could escape but Netsuke does not yet accept.
  • A literal $ in a description, depfile, deps, or pool is doubled, so it prints as written: a description of CC $out shows CC $out, not the output path. Newline, carriage return, and NUL are rejected in those fields.

Security Model

Safe by Default

Netsuke validates every manifest against a schema before template expansion runs, quotes the path markers it owns, and rejects the characters Ninja cannot represent unambiguously. Diagnostics are typed, so validation failures point to the exact manifest location rather than surfacing as opaque errors.

YAML first

The manifest must be valid YAML before any template expansion happens. Structural mistakes fail early, before commands are generated.

Quoted path markers

{{ ins }} and {{ outs }} are quoted as path arguments for the selected recipe shell. Other rendered Jinja values are not automatically shell-quoted, and the shell_escape filter described in older drafts is not implemented in v0.1.0-beta4: handwritten shell fragments remain the author's responsibility.

Typed diagnostics

CLI errors are designed to carry context, hints, and localized output rather than dropping raw parser failures on the floor.

Scoped glob authority

Since v0.1.0-beta2, the filesystem capability used for glob() metadata checks is opened at the pattern's longest literal directory prefix — not at the filesystem root or the working directory — so glob expansion holds only the authority its pattern can reach. Since v0.1.0-beta3, the Jinja helper also rejects any matched path that is not a portable unquoted shell word, so an untrusted checkout filename cannot become shell syntax when item is interpolated into a recipe.

A pattern whose literal prefix is missing, or names something other than a directory, expands to no matches. A match reached through a symbolic link that resolves outside the prefix, or dangles, is skipped rather than failing the expansion; a cyclic symbolic link still fails it, and a literal prefix that is itself a symbolic link cannot establish the capability, so the expansion fails. Glob tracing redacts caller-controlled path fields as <redacted>.

Manifest content stays out of logs

Since v0.1.0-beta4

At DEBUG and TRACE, graph-generation tracing no longer emits the manifest AST, target or dependency names, recipe commands, variable values, macro bodies, or paths. What remains is bounded: a span, fixed recipe_shell, outcome, and error_category fields, and count-only metrics. There is no replacement log, because rendered values can carry secrets that env() interpolated. Anyone who scraped --verbose output for manifest detail needs another route.

Vulnerability Mitigation

Qualitative comparison based on documented safety features; no numeric mitigation claim is implied.

Manifest schema validation, quoted path markers with shell-safe glob results, and typed diagnostics give Netsuke stronger qualitative mitigation than ad hoc scripts on each documented boundary. Interactive charting requires JavaScript.