nah nah

NAH(1) · EXTENSIONS

Extending

A custom guard is a trusted, one-shot executable supported on macOS, Linux, and Windows through the unsigned x86-64 release. It can add a block and nothing else. Keep its data, documentation, and tests together.

Human boundary

A coding agent may inspect this topic, dry-run commands, scaffold a guard, and edit the inert proposal. nah test never executes the tested command, but it does execute matching active custom guards. Trust and activation are protected changes: ordinarily the human runs nah trust, nah untrust, and nah guard enable|disable outside the session. During a suitable operator-started nap the agent may help, but any guard not paused by that mode still decides.

User guard loop:

nah guard new corp-api
# Agent edits the generated guard files under ~/.nah/guards/corp-api/.
nah guards
# Human reviews and enables the exact bytes:
nah guard enable corp-api
nah test --json "corp-api status"

nah guard new creates:

~/.nah/guards/corp-api/
  policy.toml
  run
  README.md

On Windows it instead creates run.cmd and run.py; run.cmd invokes py -3 with the adjacent Python file. The generated template therefore requires the Python Launcher with Python 3 installed.

Project guards live under <project>/.nah/guards/<name>. Create one with nah guard new corp-api --project /repo; after nah trust /repo, enable it with the same --project /repo. Untrusting the root revokes its activations. Scope flags disambiguate the same name in multiple scopes.

Changing covered bytes makes an activation needs-reapproval; review and enable it again. For missing, restore the bundle or disable its activation. nah guards also reports inactive and active.

Malformed, reserved, or colliding proposals are skipped; nah test warns without hiding healthy siblings. Once an activation exists, a missing, changed, untrusted, or unreadable activated bundle contributes an evaluation failure. The call delegates unless another guard or self-protection blocks. nah nap all is the intentional exception: it skips custom guards with the rest of non-permanent enforcement.

Manifest

name = "corp-api"
match = ["corp-api", "curl"]
protocol = "exec/v2"
provenance = "agent"       # "user" or "agent"; informational only
data = ["rules.json"]      # optional

Unknown manifest fields are rejected. A bundle with another protocol, such as an exec/v1 guard from nah 1.5.0, is skipped with unsupported-policy-protocol until its manifest and program are updated for exec/v2. A guard name is 1–64 ASCII bytes, starts and ends with a lowercase letter or digit, and otherwise contains only lowercase letters, digits, -, _, or .. Built-in guard names are reserved. Each match entry is an exact lexical program token, not a glob, command line, or regular expression. Entries must be unique and nonempty; control characters and *, ?, [, or ] are rejected. An explicit path selector matches only that path. A bare selector such as aws also matches the same name in a standard executable directory such as /bin, /usr/bin, /usr/local/bin, or macOS Homebrew's /opt/homebrew/bin and its coreutils and findutils libexec/gnubin. Filesystem guards trust the same list for /-spelled program paths. It does not match ./aws, /tmp/aws, or a project-local lookalike; name one of those paths explicitly when intended.

Selection and exec/v2 use the same public evidence. Public calls are the tool call itself and, for shell input, the exact child commands it launches with fully literal arguments and a bound working directory, such as corp-api under sudo corp-api. A child with an unresolved expansion or directory, and interpreter source, including bash -c and script launches, stay private to built-in guards. Any public call with a known identity may select a guard. A user guard is eligible everywhere. A project guard also requires the matching call's known cwd to be its trusted root or a descendant. Re-check each call rather than treating unrelated or out-of-root facts as in scope.

Use explicit parent and dataflow references; array adjacency does not establish nesting or execution.

Every data path must be unique, relative, nonempty, and made only of normal path components. policy.toml, run, run.exe, run.cmd, and run.bat cannot be data entries. Manifest, entrypoints, and data entries must be regular files, not symlinks.

macOS and Linux require an executable run. Windows requires exactly one of run.exe, run.cmd, or run.bat; a missing or ambiguous Windows entrypoint leaves the bundle inactive. A cross-platform bundle may contain run and one Windows entrypoint. Every recognized entrypoint present in the bundle, plus every declared data file, is covered by the activation hash on every platform. The Windows template declares run.py as data so its interpreter source is also covered.

Exact exec/v2 request

For every selected uncached request, nah starts the selected entrypoint with the guard directory as its working directory. The unsandboxed process inherits nah's environment. nah writes one compact UTF-8 JSON object plus a newline to standard input, closes it, and captures stdout and stderr. Inherited variables may contain credentials.

Request for corp-api delete --all, with resources, facts, occurrences, and relations omitted:

{
  "v": 2,
  "evidence": {
    "coverage": "partial",
    "complete": true,
    "calls": [{
      "id": 0,
      "parent": null,
      "kind": "Shell",
      "identity": {"Known": "Bash"},
      "arguments": "Unknown",
      "cwd": {"Known": "/repo"},
      "payload_group": {"Known": 0},
      "visibility_ordinal": {"Known": 0},
      "coverage": "partial"
    }, {
      "id": 1,
      "parent": 0,
      "kind": "Argv",
      "identity": {"Known": "corp-api"},
      "arguments": {"Known": ["corp-api", "delete", "--all"]},
      "cwd": {"Known": "/repo"},
      "payload_group": {"Known": 0},
      "visibility_ordinal": {"Known": 1},
      "coverage": "partial"
    }],
    "conditions": [],
    "gaps": [{"id": 0, "call": 0, "phase": "Analysis", "category": "Unmodeled",
      "code": "unmodeled-command", "domain": null}]
  },
  "observation": {
    "cwd": {"status": "ok", "value": "/repo"},
    "roots": {
      "status": "ok",
      "value": [{"kind": "project", "path": "/repo"}]
    }
  }
}

coverage is full or partial; partial evidence cannot prove that an unrepresented operation is absent. complete: false means the public projection omitted evidence, for example private interpreter source. IDs are request-local integers; resolve references by ID rather than array position. The public subset closes call parents and fact, resource, occurrence, relation, and condition references without exposing private calls.

Calls have a kind of Shell, Argv, VisibleCode, or Native. A known identity can select a guard even when the call's arguments are unknown. Knowledge fields use {"Known": value} or the string "Unknown". Unknown is not an empty value or a negative finding.

arguments, when known, contains exact visible command arguments including element zero, empty arguments, repeated flags, --, and --key=value spelling. Compare the array directly rather than joining it into a string. Shell and other source-bearing calls have unknown arguments. Raw tool input, shell source, inline code, and process-resource argv are excluded. Native calls expose modeled facts rather than their raw input objects.

Facts carry call, realm, certainty, modality, optional condition and occurrence bounds, and a tagged payload. For example, {"EnvironmentAccess": {...}} represents environment access, {"FilesystemSearch": {...}} a search, and {"FilesystemAccess": {...}} a filesystem operation. Filesystem operations include Read, Write, Delete, and Move; their target and optional destination reference resources. Their purpose is what the modeled command or the plan's data flow shows the access does with the contents, not a policy judgment about disclosure. Resource labels, when available, carry path scope, sensitivity, protection, and host-integrity evidence. These are knowledge fields, so a missing label must not be treated as a safe target.

Occurrences identify a call's semantic or concrete ports. Relations connect occurrence IDs and describe the modeled flow, with their own certainty and optional condition. Do not infer a flow merely because a source and sink both appear. Conditions preserve boolean expressions and exclusive alternatives; conditional evidence does not establish unconditional execution. Gaps identify incomplete interpretation using stable phase, category, and code fields.

Visible arguments and modeled resource values can still contain secrets. nah does not copy raw evidence into records, diagnostics, or feedback, but a guard's reason is memoized and sent to the runtime. Never put secrets or raw input in a reason.

Observed cwd and roots either have {"status":"ok","value":...} or {"status":"error","error":"..."}. Error values are invalid-path, not-found, permission-denied, timeout, unavailable, and non-unicode. Root kinds are project and worktree-main. Consume the JSON structurally; do not depend on object-key spacing or ordering. Inspect the exact request without execution or audit recording with nah test --json <command>, or nah test --json --runtime <runtime> --tool <name> --args-json <json> for a tool call as that runtime's hook receives it (default runtime: claude).

Exact responses

A guard blocks with exactly:

{"block":true,"reason":"delete --all blocked; use the staged cleanup"}

Otherwise it declines to act:

{"abstain":true}

Only block, abstain, and reason are accepted. Block with block: true and a nonempty reason of at most 1024 UTF-8 bytes. Reasons may contain tab or newline but no other control characters. Abstain with exactly abstain: true and no reason; it contributes nothing. No response approves a call. Make reasons actionable. Reserve prompt-injection warnings for unexpected secret, exfiltration, or hidden-code requests.

Match a dangerous shape positively and abstain from everything else:

import json
import sys

request = json.load(sys.stdin)
response = {"abstain": True}
for call in request["evidence"]["calls"]:
    identity = call["identity"]
    if not isinstance(identity, dict) or identity.get("Known") != "corp-api":
        continue
    arguments = call["arguments"]
    argv = arguments.get("Known") if isinstance(arguments, dict) else None
    if argv == ["corp-api", "delete", "--all"]:
        response = {
            "block": True,
            "reason": "corp-api delete --all requires review",
        }
        break
print(json.dumps(response))

Write one compact JSON object to stdout, optionally followed by one newline. Leading whitespace, trailing whitespace other than that newline, carriage returns, invalid UTF-8, multiple JSON values, and unknown fields are rejected. Stdout is capped at 64 KiB. Stderr is capped at 8 KiB and is diagnostic only. Execution has 750 ms on Unix and 1.5 s on Windows. Timeout or teardown kills its Unix process group or Windows Job Object, including descendants. A nonzero exit is a crash. A successful exit with empty stdout is silence. A spawn, crash, silence, timeout, transport rejection, or semantically invalid response produces a typed failure and no finding. Other guards still run; any definite finding blocks, otherwise the call delegates. Live non-dry-run dispatch attempts to persist the failure redacted. A valid abstention contributes nothing.

--fail-closed converts that delegate to a structural block. Only validated responses enter the memo cache, so failures execute again.

Selected custom guards execute sequentially, so their elapsed time accumulates within the agent runtime's hook deadline. Runtime limits and behavior vary.

nah test --json puts process outcomes under consultations: response, silence, crash, timeout, spawn-failure, or rejected-transport. Transport rejection codes are oversize, invalid-utf8, invalid-json, multiple-values, invalid-framing, and invalid-response-fields. Top-level failures carries semantic codes including ambiguous-response, missing-outcome, block-must-be-true, abstain-must-be-true, abstain-has-reason, missing-reason, reason-too-long, and invalid-reason-control.

Purity and memoization

The response must be a pure function of the request and activated bundle: do not use cross-call memory, clocks, or changing network reads. nah memoizes a validated response under a digest covering the exact serialized exec/v2 request, activation, and trusted project root. The activation includes the bundle hash. Private source and omitted evidence do not affect this key; changes to visible arguments, facts, working directories, or observations do. Raw evidence is not stored in the key. Identical hot calls can avoid a process spawn. No manifest option disables memoization.