Removed
- The optional LLM layer is reduced to a single classify-unknown job (nah-1010).
Removed the LLM ask-refinement / Layer-2 intent relaxer (the cite-or-ask
ask → allowpath, its tiered risk veto, and every per-action relax opt-in),
the visible inlinelang_execLLM review, the transcript-reading prompt context,
and thellm.eligible/llm.deny_limit/llm_risks.pymachinery. The optional
LLM (still off by default;llm.mode: on) can no longer relax a knownask,
review inline code, or read your conversation — it only classifies unknowns
(see Added). Claude and Codex share this one path. - Removed the LLM write content-review gate (nah-997) that inspected
Write/Edit/MultiEdit/NotebookEdit and Codexapply_patchpayloads as data-at-rest
and could escalate a cleanallowtoask. Write-like tools are now guarded by
the deterministic floor only — sensitive-path block, project-boundary, and
destructive-patch checks — which is cheap, clear, and unchanged. - Removed the session taint tracking and provenance features entirely
(src/nah/taint.py,src/nah/provenance.py) along with all runtime wiring
(Claudehook.py, Codexcodex_hooks.py/codex_run.py, terminal guard), the
taint/provenanceconfig surface, the LLM provenance-review path, and the
log/message rendering and docs (nah-1009). Both were opt-in and off by default,
so removal is behavior-neutral for current users; the deterministic classifier,
LLM classify-unknown path, and the 43 action types are unchanged. The non-headless
CodexPreToolUsehook is now fully observation-inert (its only job was taint
state); enforcement still happens atPermissionRequest. - Removed deterministic secret-looking and credential-path content scanning, along with
secret redaction on LLM prompt/transcript context and local post-tool error summaries.
Secret protection now relies on structural controls such as sensitive paths,
credential-search detection, and explicit secret-store/env reads
rather than guessing token-shaped text in write payloads (nah-1006). - Removed the
/nah-demoClaude Code showcase and its curated cases
(src/nah/demo_cases.py,src/nah/data/nah_demo.json, the.claude/commands/nah-demo.md
slash command, andtests/test_nah_demo.py). It was a product demo, not part of the
guard or the regression suite;pytestremains the coverage source and
nah audit-threat-modelthe coverage report.
Added
- Optional LLM classify-unknown (nah-982, nah-994). When the deterministic
classifier returnsunknownfor a Bash command, the optional LLM (still off by
default;llm.mode: on) maps it to a built-in action type and the kind-tagged
targets it touches. The mapped type re-enters the normal policy machinery and
each surfacedpath/hosttarget is re-checked through the same deterministic
floor (sensitive paths, project boundary, known hosts): the LLM extracts, the
floor matches.db/containertargets have no faithfully-mirrorable floor (the
real db/container floors are policy-/cwd-/exec-specific), so they stay
unverifiable and the mapped type's policy decides — allow-policy safe reads
clear, context-policy execs ask (nah-994). A read of~/.sshis never
auto-allowed; an unverifiable target falls back to ask; an obfuscated unknown can
tighten to block. Fail-closed, process-cached, and command-only (no transcript).
entry["llm"]records the classify pass with a top-levelaction_type_source
(deterministic|llm_classify) and a newnah log --classifiedfilter;
nah testshows the classification and per-target floor verdicts. - Flag-aware
env_readclassification for shell builtins,ps, andcaddy fmt(nah-1005).
Follow-up to nah-1004 covering the cases a static prefix table can't express because the
safe and unsafe forms are the same command split by flags:- bare
env(no inner command), bareset, and bare/-pexport/declare/typeset→
env_read(ask), while their assignment, option (set -x), and exec-wrapper
(env FOO=bar cmd) forms keep their existing classification. pswith the BSD environment modifier (ps e,ps eww,ps auxe) →env_read, while
SysVps -e/-ef(all processes) and value-flag forms (ps -u <user>,ps -o pid,etime) correctly stayfilesystem_read— the classifier is value-flag-aware to
avoid false positives.caddy fmt --overwrite→filesystem_write; barecaddy fmt→filesystem_read.- Removes the now-redundant static
export -p/declare -p/typeset -pentries from the
env_readtable (the builtin classifier owns them).
- bare
service_inspectandenv_readaction types;service_readnarrowed to remote (nah-1004).
service_readwas overloaded: its static table was 100% local daemon inspection
(systemctl status,journalctl) while every remote API read (curl GET, gRPC,
GraphQL) was classified dynamically, so its singlecontextpolicy fit only the
remote half and the audit label ("remote API state") was wrong for the local half.service_inspect(policyallow) is the honest home for local service/daemon
inspection — the systemd entries move here, joined bycaddy version/list-modules,
launchctl list/print,sc query/queryex/qc,rc-status/rc-service -l, and
service --status-all. It is deliberately kept out of the data-egress
boundary (local inspection is not network egress).env_read(policyask) is the honest home for commands whose purpose is
exposing environment or secret values —printenv,caddy environ,
systemctl show-environment,export -p/declare -p/typeset -p, and secret-store
reads (vault read/kv get,aws secretsmanager get-secret-value,
aws ssm get-parameter,gcloud secrets versions access,az keyvault secret show,
kubectl get/describe secret,pass show,op read/item get,bw get,
heroku config,doppler secrets,infisical secrets,chamber read/export,
sops -d). These were previouslyunknown → ask, which lied in the audit log and
fired a wasted LLM classify on every invocation.systemctl show-environmentmoves
from a silentservice_read → allowto an honestenv_read → ask. Name-only listers
(gh secret list, etc.) are intentionally excluded; secret-injecting exec wrappers
(op run,doppler run,aws-vault exec) stay on the exec path. Flag-dependent
forms (bareenv/set/export,psenv-flags,caddy fmt --overwrite) are
deferred to a follow-up (nah-1005). Also classifiescrontab -landcaddy validate
asfilesystem_read.
- talosctl global flag stripping before subcommand classification —
talosctl -n <ip> get routes,talosctl --nodes=<ip> dmesg, and other talosctl commands that carry connection global flags (-n/--nodes,-e/--endpoints,-c/--cluster,--context,--talosconfig) now strip those flags before the global-table prefix match instead of falling through tounknown. Mirrors the kubectl/flux idiom and fails closed: unknown or malformed pre-subcommand flags stay on theunknownask path, and dangerous subcommands such astalosctl reboot/talosctl resetstill classify as configured. Closes #86; PR #89 by @srgvg. - flux global flag stripping before subcommand classification —
flux -n <ns> get kustomizations,flux --namespace=<ns> list, and other flux commands that carry kubeconfig-style global flags (-n/--namespace,--context,--kubeconfig,--timeout,--token, ...) now strip those flags before the global-table prefix match instead of falling through tounknown. Mirrors the kubectl/talosctl idiom and fails closed: unknown or malformed pre-subcommand flags stay on theunknownask path, and destructive subcommands such asflux delete/flux uninstallstill classify as configured. Closes #87; PR #90 by @srgvg. - Codex hook-timeout probe —
nah run codex --probe[=DELAY]arms a
debug-only stall in nah's Codex hooks (gated behindNAH_HOOK_PROBE, capped
at 60s, verdict unchanged) so you can observe the timeout Codex actually
enforces.nah run codex --measure-hook-timeoutdrives Codex with the probe
and reports enforced-vs-configured timeouts, defaulting toPostToolUse(the
only event that both fires and is enforced under headlesscodex exec).
Documented in the CLI reference.
Changed
- Terminal Guard is deterministic-only (nah-985). The interactive bash/zsh
terminal guard has no LLM step. A command you type directly into your shell is
already your own intent, so there is no agent transcript to mine — the guard
classifies to allow / ask / block and anaskis confirmed inline at the
prompt. The sharedllm.modeandtargets.bash.llm.mode/targets.zsh.llm.mode
knobs are still accepted for backward compatibility but no longer affect terminal
decisions. - Container write taxonomy split by verifiable risk axis (nah-996).
container_writeis replaced bycontainer_lifecycleand
container_build. Lifecycle operations that act on named containers
(docker stop api,podman restart worker) arecontextpolicy and use
trusted_containers: every flag-free identity must be trusted, while flags,
dynamic names, and compose lifecycle commands ask. Build/image/infra commands
(docker build,docker compose build,docker network create) are
container_buildwith defaultallowand no cwd gate; autonomous presets can
tighten it withactions: {container_build: block}. Legacy
container_writeinactions:fans out to both new types,classify:maps
to conservativecontainer_lifecycle, and interactiveallow/deny/
classify/forgetcommands now ask users to choose one of the new types. - Database taxonomy gates SQL-exec capability, not SQL intent (nah-995).
Replacesdb_read/db_writewithdb_safe/db_exec: structurally-safe
database surfaces such asdolt log/status/diff/branchand Supabase list/get
tools aredb_safe(allow), while tools that can run caller-supplied SQL
aredb_exec(context) and continue to usedb_targetsfor target-scoped
allow. The olddb_read/db_writeconfig names are accepted as deprecated
aliases and canonicalized with a one-time warning. The previous
sqlite3 -readonlyandPGOPTIONS/psql -Xread-only special cases are
removed; those invocations now classify asdb_execand ask unless
db_targetsallows the target. - Layer 1 classifies into built-in types only (nah-992). The classify-unknown
pass is not offered the user's custom action types — it maps into the built-in
taxonomy only. This stops the model from collapsing a whole unknown compound into
a trusted customallowtype (e.g. acd repo && molds … && molds wontdo …
block landing on a custommolds_safe → allow). A custom type the model names
anyway is coerced tounknown, so the deterministic ask stands. - Codex lifecycle commands normalized to
nah <command> codex(nah-960).
nah status codex(read-only preflight), a new top-levelnah setup codex,
andnah uninstall codexnow match theinstall/statusshape used by every
other runtime;nah run codexis unchanged. Breaking: the old
nah codex doctor/nah codex setup/nah codex remove-setupsubcommands
are removed (no aliases) and exit nonzero — usenah status codex/
nah setup codex/nah uninstall codexinstead.nah status codexalso
fixes a silent no-op (it used to parse and exit0with no output) and is
strictly read-only: it reports missing or stale rules and exits nonzero
without creating them.nah doctor codexandnah doctor claudenow point to
nah status …. The hook-timeout probe moved fromnah codex measure-hook-timeoutto thenah run codex --measure-hook-timeoutdebug mode.
Fixed
nah testdry-runs no longer self-flag on sensitive paths in their arguments
(nah-qb3). Anah testinvocation whose arguments named a sensitive path as a
bareword or flag value (e.g.nah test --tool Read ~/.ssh/id_rsa) was flagged by
nah's own hook as a real sensitive access and paused for approval, even though
nah testis a pure dry-run classifier with no filesystem or execution side
effects. The_classify_nah_cliclassifier now recognizesnah testand allows
it without scanning its argument tokens for sensitive paths. Output redirections
(caught by the redirect guard) and command/process substitutions (classified
independently upstream) stay guarded, and the exemption is exact-match and
stage-local, so adjacent stages likenah test foo && rm -rf ~/.sshare unaffected.