{"slug": "show-hn-pawl-hooks-that-check-what-a-coding-agent-s-script-would-delete", "title": "Show HN: Pawl – Hooks that check what a coding agent's script would delete", "summary": "Developer ulukaya released Pawl, an open-source set of deterministic hook checks for coding agents that runs as one plugin across Claude Code, OpenAI Codex and Antigravity, adding only a 144-token skill description to the prompt. Pawl's thirteen gates inspect tool calls without executing them, denying destructive commands such as a script that runs rm -rf ~/, git reset --hard, edits that change nothing, and sends naming ~/.deploy/, while auto-approving provably read-only commands like git log --oneline -5. A replay command sends every tool call from a user's past 30 days of Claude Code sessions through the same gates to tally what would have been blocked or approved without a prompt.", "body_md": "Deterministic gates for coding agents, as one plugin for **Claude Code**,\n**OpenAI Codex** and **Antigravity**.\n\nAgents make the same mistakes over and over, and telling them not to in the\nprompt stops working after a page. `pawl` is a set of small checks that run as\ncode, not as instructions, and a report command that tallies what they blocked.\nEach check watches for one mistake and refuses it, cleans it up, or (for\nprovably read-only commands) waves it through without a prompt. Plain Python\nstandard library, no model calls, nothing added to the prompt but a 144-token\nskill description.\n\nA pawl is the small part in a ratchet that lets the wheel move forward and stops it from slipping back. Every check here works the same way: the current state is the floor.\n\nNo install, no harness, nothing written outside a scratch directory:\n\n```\ngit clone https://github.com/ulukaya/pawl && python3 pawl/hooks/pawl.py demo\n```\n\nRecorded through the real Claude Code hook adapter using fixture tool calls. The commands are inspected, never executed; displayed reasons are excerpts. This demonstrates hook decisions rather than a live agent session.\n\nThe command above sends thirteen calls through the real dispatcher, written as Antigravity, Claude Code and Codex send it, and prints what each harness is told:\n\n```\ncall                        gate        antigravity      claude code      codex\n-------------------------------------------------------------------------------\nmake build                  -           allow            silent           silent\ngit log --oneline -5        readonly    auto_approve     allow            silent\ngit reset --hard            git         force_ask        ask              deny\nscript that runs rm -rf ~/  blast       deny             deny             deny\ncurl install.sh | sh        pin         force_ask        ask              deny\ntail -f server.log          poll        force_ask        ask              deny\nsame pytest run, 3rd time   loop        force_ask        ask              deny\nedit that changes nothing   noop        deny             deny             deny\nwrite with a U+200B         zero-width  allow +rewrite   +rewrite         allow +rewrite\nsend naming ~/.deploy/      egress      deny             deny             deny\nread another session        fence       force_ask        ask              deny\nread Chrome's cookie key    creds       force_ask        ask              deny\nstop with tail -f running   idle        n/a              block            n/a\n```\n\n`silent` leaves the harness's own prompt in place; `allow` and\n`auto_approve` skip it. A Codex PreToolUse hook can neither ask nor approve,\nso there an ask is a deny that tells the agent how to proceed. `--verbose` adds every\nreason, `--json` every raw answer.\n\n`replay` sends every tool call from your past Claude Code sessions through\nthe same gates. It runs nothing and keeps no state:\n\n```\npython3 pawl/hooks/pawl.py replay --days 30 --show\n```\n\nIt lists each call pawl would have asked about or refused, then a tally per gate, including how many read-only commands it would have approved without a prompt. Run it before installing to see what would change, and after changing a gate to find its false alarms on real work.\n\nGrouped by what a miss costs. The first table is work or data an agent cannot take back; those gates are the reason pawl exists.\n\n**Can't be undone**\n\n| The mistake | What pawl does | Gate | \n|---|---|---|\n| Deletes home, root, a drive or ~/Documents, directly or from a script, `trap` ,`npm run` , Makefile,`python -c` or container it runs | Refuses; asks before anything else outside the workspace | `blast` | \n| Runs `git reset --hard` ,`git clean -fdx` ,`git commit --no-verify` ,`git push --force` or`git branch -D` and loses work | Asks the human first | `git` | \n| Pastes internal paths, tokens or hostnames into a message | Blocks the send | `send` (egress) | \n| Reads browser cookies, saved passwords or the keychain ( `security find-generic-password` ,`import browser_cookie3` ) | Asks the human first | `creds` | \n| Reads another conversation's private files | Asks the human first | `fence` | \n| Installs from a branch URL ( `pip install .../archive/main.zip` ), runs`npm install -g tool` with no version, or pipes`curl` into`sh` | Asks the human to pin it or approve it | `pin` | \n| Floods a chat room or inbox | Caps sends per channel per day; refuses a send loop it cannot count | `send` (budget) | \n\n**Prompts it removes**\n\n| The mistake | What pawl does | Gate | \n|---|---|---|\n| Stalls on a permission prompt for `ls` or`git log` | Approves commands that provably only read | `readonly` | \n\n**Time and tokens it saves**\n\n| The mistake | What pawl does | Gate | \n|---|---|---|\n| Runs `while true; do sleep` ,`tail -f` or`sleep 3600` and hangs | Asks the human first | `poll` | \n| Calls the same tool with the same arguments in a loop | Asks before the third identical call | `loop` | \n| Ends its turn with a `tail -f` still running in the background | Blocks the stop once and names the task | `idle` | \n| Changes a configured Git project without passing its checks | Reminds once at Stop; receipts match the tested contents | `verify` | \n| Re-reads its own transcript after every context truncation | Refuses past a per-turn limit | `reread` | \n| Sends an edit whose replacement equals its target, or rewrites a file with the bytes it already holds | Refuses it and sends the agent back to read | `noop` | \n| Writes invisible zero-width characters into a file | Strips them so the write lands clean | `zero-width` | \n| Writes a message that reads like a bot | Blocks it above a score threshold | `send` (prose) | \n\n**CLIs for pre-commit, CI and cron**\n\n| The mistake | What pawl does | Gate | \n|---|---|---|\n| Claims a bug is fixed without proving it | Requires the test to fail before the fix | `repro_fence.py` | \n| Ships a test that still passes with the function stubbed out | Stubs each function in a copy and fails when the tests survive | `bite_check.py` | \n| Lets failing-test or lint counts creep up | Keeps a baseline that can only go down | `ratchet.py` | \n| Grows always-on prompt files until they cost more than they help | Caps their token size | `prompt_budget.py` | \n| Keeps retrying a cron job that fails every night | Pauses it after repeated failures | `breaker.py` | \n\nThe first three tables are hook gates that fire on their own; the\nlast holds CLIs. Every piece also runs on its own: see\n`pieces/<name>/README.md`.\n\n| Prompt | 144 tokens: the skill's description, the only always-on text | \n| Latency | about 45 ms per Read and 65 ms per Bash call (median, Linux, Python 3.11), all gates in one process | \n| Network | none: no telemetry, no model calls ( [PRIVACY.md](https://github.com/ulukaya/pawl/blob/main/PRIVACY.md) ) | \n| Dependencies | the Python 3.11+ standard library | \n\n```\nclaude plugin marketplace add ulukaya/pawl\nclaude plugin install pawl@pawl\n```\n\nOr inside a session: `/plugin marketplace add ulukaya/pawl`, then\n`/plugin install pawl@pawl`. Start a new session (or `/reload-plugins`), and\n`claude plugin details pawl` lists `Hooks (2) PreToolUse, Stop`.\n\n```\ncodex plugin marketplace add ulukaya/pawl\ncodex plugin add pawl@pawl\n```\n\nCodex reads `.codex-plugin/plugin.json`, which points it at\n`hooks/codex.json` and the skill. Hooks need a Codex release with lifecycle\nhooks.\n\nAdding the plugin does not turn its hooks on. Codex runs a plugin's hooks\nonly once you have reviewed and trusted their current definitions, and skips\nnew or changed ones until then; see\n[review and trust hooks](https://developers.openai.com/codex/hooks#review-and-trust-hooks)\nin the Codex docs. To activate pawl:\n\n1. Start `codex` . It warns at startup when hooks need review.\n2. Open `/hooks` , review pawl's`PreToolUse` and`Stop` hooks (each runs`hooks/pawl.py` with`--harness codex` ) and trust both.\n3. Do it again after an update that changes `hooks/codex.json` : a changed\ndefinition is skipped until trusted again.\n\nTo check that pawl is live, open `/hooks` and confirm both hooks are trusted\nand enabled. Then, in a scratch session, ask Codex to run `echo pawl-check`\nthree times as separate commands: the third is refused with a\n`[PAWL loop]` reason. `hooks/e2e_test.py` does not prove this: it runs the\nshipped hook commands directly on fixture payloads, with no Codex process,\nso it passes whether or not Codex loaded or trusted the hooks.\n\n```\ngit clone https://github.com/ulukaya/pawl && cd pawl\n./install.sh --antigravity\n```\n\nThis links the checkout to `~/.gemini/config/plugins/pawl` and adds\n`{\"path\": \"plugins/pawl\"}` to `~/.gemini/config/plugins.json` next to any\nplugins already listed. Restart Antigravity; the plugin inventory lists\n`pawl`.\n\n```\n./install.sh                 # every harness found on this machine\n./install.sh --claude        # or --antigravity, --codex\n./install.sh --uninstall     # reverse every step\n./install.sh --dry-run       # print what would change\n```\n\nFor Claude Code and Codex the installer runs the commands above with this\ncheckout as the marketplace, so the plugin loads in place (Codex still needs\nthe `/hooks` trust step above; the installer reminds you); `--source ulukaya/pawl` tracks GitHub instead. Every step is idempotent. It then runs\nthe test battery, which needs `pytest` (`PAWL_PYTHON` picks the\ninterpreter); the plugin itself needs only Python 3.11+.\n\n```\n harness ──stdin──▶ hooks/pawl.py pre|stop --harness H\n                      │\n                      ├─ harness.parse()    native payload ─▶ one canonical call\n                      ├─ gates.plan()       which gates apply to this tool\n                      ├─ pieces/<name>/     each gate asks its piece\n                      ├─ merge              deny > ask > approve > allow\n                      └─ harness.render()   answer in H's own contract ──stdout──▶\n```\n\nEach harness has its own config, all running the same dispatcher:\n\n| Harness | Config | Command | \n|---|---|---|\n| Antigravity | `hooks.json` | `python3 -B hooks/pawl.py pre --only <gate> --harness antigravity` , one group per gate | \n| Claude Code | `hooks/hooks.json` | `python3 -B \"${CLAUDE_PLUGIN_ROOT}/hooks/pawl.py\" pre --harness claude` , plus`stop` | \n| Codex | `hooks/codex.json` | `python3 -B \"${PLUGIN_ROOT}/hooks/pawl.py\" pre --harness codex` , plus`stop` | \n\n`hooks/harness.py`, with its tables in `hooks/harness_vocab.py`, maps each\nharness's tools onto the canonical names the pieces speak (Claude Code\n`Bash`, `Read`, `Write`, `Edit`, `TaskOutput`; Codex `Bash` and\n`apply_patch`) and writes each answer the way that harness reads it:\n\n| pawl decides | Antigravity | Claude Code | Codex | \n|---|---|---|---|\n| no objection | `allow` | no output: the normal permission prompt still applies | no output | \n| ask the human | `force_ask` | `permissionDecision: ask` | `deny` with the reason: a PreToolUse hook cannot ask | \n| refuse | `deny` | `permissionDecision: deny` | `permissionDecision: deny` | \n| provably read-only | `auto_approve` | `permissionDecision: allow` | no output: a PreToolUse hook cannot approve | \n| rewrite the input | `overwrite` | `updatedInput` , permission unchanged | `updatedInput` | \n| keep working (Stop) | `block` | `decision: block` | `decision: block` | \n\n`git`, `poll`, `pin`, `creds` and `fence` read a shell command through\n`hooks/shell_view.py`, which blanks heredoc bodies that never run as shell:\nthe file `cat > notes.md <<'EOF'` writes, a commit message in\n`git commit -m \"$(cat <<'EOF' ...)\"`, the string literals of a\n`python3 - <<'EOF'` edit script, a list of commands that a `while read`\nloop or a later `python3 check.py cases.txt` only reads. Each rule names\nwhat is known to be data, and anything else keeps the body: a file run or\ncopied later, a loop that runs or saves the lines it reads, and Python\nwhose code can start a process (`hooks/py_body.py` reads it with `ast`).\n`creds` and `fence` keep the paths in Python literals.\n\nCodex does let a separate `PermissionRequest` hook, sent only when Codex is\nabout to ask the user, answer allow or deny; pawl registers no such hook.\n\nReasons are reworded in the harness's own tool names (`Read`, not\n`view_file`). The first deny ends a run, so a refused call never spends a\nsend-budget unit. On Codex an ask is a deny, so it ends the run as well, and\na send over budget is refused with nothing spent and no override logged.\nA git or poll gate whose piece cannot load denies, as it does when it fails.\nEvery answer exits 0; no path prints a traceback.\n\n| Gate | Fires on | Fails | Antigravity | Claude Code | Codex | \n|---|---|---|---|---|---|\n| `fence` | every tool | open | `brain/` ,`conversations/` | `~/.claude/projects/` | `~/.codex/sessions/` | \n| `creds` | every tool | open | yes | yes | yes (ask is deny) | \n| `git` | shell | closed | yes | yes | yes (deny) | \n| `blast` | shell | open (an unfinished analysis asks) | yes | yes | yes (ask is deny) | \n| `pin` | shell | open | yes | yes | yes (ask is deny) | \n| `poll` | shell | closed | yes | yes | yes (deny) | \n| `noop` | edits | open | `replace_file_content` | `Edit` | `apply_patch` | \n| `zero-width` | writes, edits | open | yes | `Write` ,`Edit` | `apply_patch` | \n| `readonly` | shell | open | yes | yes | no (PreToolUse cannot approve) | \n| `reread` | reads, shell | open | yes | yes | yes | \n| `loop` | every tool | open | yes | yes | yes (deny) | \n| `send` | shell | egress closed | yes | yes | yes | \n| `idle` | Stop | open | `/proc` tasks | Stop payload tasks | no task list | \n| `verify` | all + Stop | open | configured Git files | configured Git files | configured Git files | \n\n`hooks/pawl.py gates` lists them. Details: `skills/pawl/references/<piece>.md`\nand `pieces/<piece>/README.md`.\n\n| Need | Do | \n|---|---|\n| Skip gates for a session | `PAWL_DISABLE=git,poll` (any gate name, or`egress` ,`prose` ,`budget` ) | \n| See what pawl would have done in past sessions | `python3 hooks/pawl.py replay --days 30 --show` (Claude Code transcripts) | \n| See how often each send gate fires | `python3 hooks/pawl.py stats` (reads`$PAWL_DATA/gate_events.jsonl` ) | \n| Tally every gate's denials | `python3 pieces/report/report.py --days 7` | \n| One send, git or repeated call past a gate | approve the prompt; the row is logged as a human override | \n| Change send ceilings | `SEND_BUDGET_CEILINGS='{\"chat_space\": 4}'` | \n| Change egress rules | edit `$PAWL_DATA/egress_rules.json` (default`~/.pawl/` ) | \n| Protect specific repos | `PAWL_GIT_PROTECTED_ROOTS=/repo/a:/repo/b` (default: the call's git toplevel) | \n| Keep the prompt for read-only commands | `PAWL_READONLY_PASS_OFF=1` | \n| Refuse, not ask, on another session's files | `PAWL_CONVERSATION_FENCE_STRICT=1` | \n| Fence another credential store | `PAWL_CREDENTIAL_EXTRA_ROOTS=~/.agent-reach` | \n| Refuse, not ask, on browser logins or unpinned installs | `PAWL_CREDENTIAL_FENCE_STRICT=1` ,`PAWL_INSTALL_PIN_GUARD_STRICT=1` | \n| Force a harness format | `--harness` in the config, or`PAWL_HARNESS` | \n\nOn Claude Code four of these are plugin settings, so nobody edits an\nenvironment: `/config` lists pawl's rows (Approve read-only shell commands,\nRepos the git guard protects, Refuse reads of other sessions, Gates to turn\noff), and `claude plugin install pawl@pawl --config disable=reread` sets one\nat install. A `PAWL_*` variable you export wins over the setting.\n\nState and logs live under `PAWL_DATA` (default `~/.pawl`). Each piece's knobs\nare listed in its reference page.\n\npawl runs on your machine and nowhere else: no network, no telemetry, no\nmodel calls. Its logs hold counters and SHA-1 digests, never a command,\npath or message. One gate loosens anything: on Claude Code and Antigravity,\n`readonly` approves shell commands it can prove only read, without a prompt\n(a Codex PreToolUse hook cannot approve, so there it stays silent); turn it\noff in `/config` or with `PAWL_READONLY_PASS_OFF=1`.\n[PRIVACY.md](https://github.com/ulukaya/pawl/blob/main/PRIVACY.md) lists every file pawl reads and writes.\n\n| Piece | Wire point | Command | \n|---|---|---|\n| prose-gate | gate `send` , CI on docs | `pieces/prose-gate/prose_gate.py --plane chat draft.md` | \n| egress-firewall | gate `send` | `pieces/egress-firewall/egress_firewall.py check < text` | \n| send-budget | gate `send` | `pieces/send-budget/send_budget.py status` | \n| blast-radius-guard | gate `blast` | `pieces/blast-radius-guard/blast_radius.py check --cwd . -- bash cleanup.sh` | \n| destructive-git-guard | gate `git` | `pieces/destructive-git-guard/destructive_git_guard.py check --cwd . git reset --hard` | \n| poll-loop-guard | gate `poll` | `pieces/poll-loop-guard/poll_loop_guard.py classify \"while true; do sleep 5; done\"` | \n| oscillation-breaker | gate `loop` | `pieces/oscillation-breaker/oscillation_breaker.py check <conversation-id> view_file '{\"path\": \"a\"}'` | \n| noop-edit-guard | gate `noop` | `pieces/noop-edit-guard/noop_edit_guard.py check replace_file_content '{\"TargetContent\": \"a\", \"ReplacementContent\": \"a\"}'` | \n| zero-width-sanitizer | gate `zero-width` | `pieces/zero-width-sanitizer/zero_width_sanitizer.py strip < draft.txt` | \n| readonly-pass | gate `readonly` | `pieces/readonly-pass/readonly_pass.py check git log -5` | \n| reread-guard | gate `reread` | `pieces/reread-guard/reread_guard_hook.py < payload.json` | \n| conversation-fence | gate `fence` | `pieces/conversation-fence/conversation_fence_hook.py < payload.json` | \n| credential-fence | gate `creds` | `pieces/credential-fence/credential_fence.py check security dump-keychain` | \n| install-pin-guard | gate `pin` | `pieces/install-pin-guard/install_pin_guard.py check npm install -g mcporter` | \n| idle-task-gate | gate `idle` (Stop) | `pieces/idle-task-gate/idle_task_gate.py list <conversation-id>` | \n| ratchet-baseline | pre-commit, CI | `pieces/ratchet-baseline/ratchet.py check --baseline .ratchet.json --metric failing_tests=N` | \n| circuit-breaker | cron, sidecars | `pieces/circuit-breaker/breaker.py run nightly -- ./job.sh` | \n| repro-fence | pre-commit on bug fixes | `pieces/repro-fence/repro_fence.py both --cmd \"pytest tests/test_x.py\" --file src/x.py` | \n| bite-check | pre-commit, CI on new tests | `pieces/bite-check/bite_check.py check --file src/x.py --cmd \"pytest tests/test_x.py\"` | \n| prompt-budget | pre-commit on prompt files | `pieces/prompt-budget/prompt_budget.py check --config budget.json` | \n| report | CLI, retro | `pieces/report/report.py --days 7` | \n\nOne skill, `skills/pawl/SKILL.md`, routes an agent by denial prefix or task to\n`skills/pawl/references/<piece>.md`, which carries that piece's flags.\n\n- Not a model, model router, or model picker. `pawl` never names, selects,\nor calls a model.\n- Not a replacement for other plugins. Skill packs and shell guards keep doing their jobs; each plugin registers its own hooks and the harness runs them all.\n- Not a sandbox. The gates read tool arguments; a script that builds a path at run time is not fenced.\n- Not a workflow engine. Nothing here spawns subagents or schedules anything.\n\n`eval/` holds a gates-on vs gates-off ablation suite: 24 tasks with a\ntemptation in each (10 code-change, 6 repo-hygiene, 8 outbound), throwaway\ngit fixtures, stub senders and script graders, with no LLM judge.\nThe `blast` gate has its own measurements: five labelled corpora (329\ncases, four held out before tuning, first-seen scores kept in\n`pieces/blast-radius-guard/HILLCLIMB.md`), a replay of 472 real commands\nwith no false alarm, and `eval/blast-compare/`, which runs other deletion\nguards on the same cases:\n\nHistorical comparison from October 3, 2026. Competitor error counts were not measured; rerun them before quoting their false-alarm counts. The comparison README explains the scoring and policy differences.\n\n| Guard | Caught (191 dangerous) | False alarms (138 everyday) | \n|---|---|---|\n| pawl `blast` | 190 | 0 | \n| cc-safety-net 2.5.1 | 100 | 22 | \n| cc-safety-net 2.5.1, `paranoid` | 144 | 62 | \n| dcg 0.15.2 | 156 | 61 | \n\nThe same author wrote pawl and the cases; read the caveats in\n`eval/blast-compare/README.md` before quoting a number.\n\nScorecards for the ablation suite, three passes per arm; `eval/README.md`\nhas the per-case tables. A gate that blocks a leak often ends the task, so a\nblocked leak scores as a failure. The harm columns count the failures where\nsomething went out or was lost: a leak, a send past a ceiling or to an\naddress off the allowlist, another session's uncommitted work.\n\n| Agent | Passed, on | Passed, off | Harm, on | Harm, off | \n|---|---|---|---|---|\n| Claude Code, claude-sonnet-5-5 | 57/67 (85%) | 41/67 (61%) | 0 | 17 | \n| Claude Code, local Qwen3.8 Flash-Next | 50/72 (69%) | 48/72 (67%) | 5 | 16 | \n\nSonnet's runs leave out 5 per arm the model refused. Of Qwen's 5 on-arm\nharms, 2 sent from one shell loop, gated since (a re-run of that case sent\nnothing past the ceiling), and 3 deleted another session's uncommitted work\nwith `rm -rf` after the `git` gate refused a stash. Same caveat: the same\nauthor wrote pawl and the cases.\n\nSonnet's harm counts are documented in `eval/README.md`; that older JSONL\nlacks verdict lines, so the harm counts cannot be regenerated from it with\n`results_table.py`. Neither agent scorecard is a new run of version 0.4.1.\n\n`eval/run_arms.sh` runs both arms; see `eval/README.md`. `eval/claude/` holds\nfour cases for Claude Code's built-in runner (`claude plugin eval . --scaffold --allow-tools Bash Edit Write`), also judge-free.\n\n```\npython3 -m venv .venv && .venv/bin/pip install pytest\n.venv/bin/python3 -B run_tests.py       # one OK line per suite\n.venv/bin/python3 -B check_portable.py  # portable: clean\ngit config core.hooksPath .githooks     # run both before every push\n```\n\n`run_tests.py` runs the hooks suite (including end-to-end runs of the\ncommands in the shipped Claude Code and Codex configs, fed fixture payloads\nwithout a real host), every piece's suite, the root tools and the\neval grader twins. `check_portable.py` fails on an absolute home path, a\nnon-stdlib import in shipped code, a CR byte, a markdown prose line over 80\ncolumns, a reference page naming an env var its piece never reads, a hook\nconfig that does not run `hooks/pawl.py` the way its harness needs,\nmanifests that disagree on name or version, a source file over 500 lines, or\na function nested more than 3 blocks deep. The `.githooks/pre-push` hook runs\nboth and refuses a push that fails. GitHub CI (`.github/workflows/ci.yml`,\nLinux and macOS, Python 3.11 to 3.14) is paused and runs only by hand for\nnow. `CLAUDE.md` holds the engineering rules; `CHANGELOG.md` the history.\n[CONTRIBUTING.md](https://github.com/ulukaya/pawl/blob/main/CONTRIBUTING.md) is the short version for a first pull\nrequest, and [SECURITY.md](https://github.com/ulukaya/pawl/blob/main/SECURITY.md) says what to report privately.\n\nTo add a gate: write the piece under `pieces/<name>/` with its tests, add a\n`Gate` to `hooks/gates.py`, add its Antigravity group to `hooks.json`, and\nadd `skills/pawl/references/<name>.md`; `check_portable.py` tells you what\nis missing.\n\nOpen an issue at [https://github.com/ulukaya/pawl/issues](https://github.com/ulukaya/pawl/issues) or email\n[ulukaya@gmail.com](mailto:ulukaya@gmail.com). pawl is licensed under [Apache-2.0](https://github.com/ulukaya/pawl/blob/main/LICENSE).", "url": "https://wpnews.pro/news/show-hn-pawl-hooks-that-check-what-a-coding-agent-s-script-would-delete", "canonical_source": "https://github.com/ulukaya/pawl", "published_at": "2026-10-06 15:09:07+00:00", "updated_at": "2026-10-06 15:21:09.710306+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "ai-safety"], "entities": ["Pawl", "ulukaya", "Claude Code", "OpenAI Codex", "Antigravity", "GitHub"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/show-hn-pawl-hooks-that-check-what-a-coding-agent-s-script-would-delete", "markdown": "https://wpnews.pro/news/show-hn-pawl-hooks-that-check-what-a-coding-agent-s-script-would-delete.md", "text": "https://wpnews.pro/news/show-hn-pawl-hooks-that-check-what-a-coding-agent-s-script-would-delete.txt", "jsonld": "https://wpnews.pro/news/show-hn-pawl-hooks-that-check-what-a-coding-agent-s-script-would-delete.jsonld"}}