diff --git a/.claude/hooks/ask-before-risky-commands.sh b/.claude/hooks/ask-before-risky-commands.sh
new file mode 100755
index 0000000..4f8788c
--- /dev/null
+++ b/.claude/hooks/ask-before-risky-commands.sh
@@ -0,0 +1,69 @@
+#!/usr/bin/env bash
+# Claude Code PreToolUse(Bash) gate — pathscale backend service.
+#
+# Prompts before prod-affecting / destructive commands in ANY wrapper form
+# (env VAR=val …, tool -C
…, chained with ; && |, multi-line, quoted via
+# bash -c '…'). For everything else it stays SILENT (exit 0, no decision), so the
+# normal permission rules — permissions.allow / ask / deny in .claude/settings.json
+# and the session's permission mode — decide as usual.
+#
+# This is one layer of defense, not a replacement for the permission system: a
+# pattern match over a command string is best-effort (quoting and indirection can
+# evade any blocklist). It backs up the declarative permissions.ask list in
+# .claude/settings.json — KEEP THE TWO IN SYNC — and for fully autonomous runs,
+# prefer OS-level sandboxing on top.
+#
+# Adapted from PakhomovAlexander/project-hub. Edit RISKY_WORDS for this repo;
+# tooling; further branches below gate `git`/`docker push`, `git clean`, recursive
+# `rm`, `find -delete`, package publishing, PR-merge/release via `gh`, and a deploy
+# script run by path. Add your own command families (e.g. `ssh`, a bespoke deploy
+# CLI) to RISKY_WORDS, or trim what you don't use — and mirror the change in
+# permissions.ask in .claude/settings.json.
+set -u
+
+# --- the watchlist: command words that should prompt before running ----------------
+RISKY_WORDS="aws|gcloud|az|kubectl|helm|terraform|terragrunt|flyctl|fly"
+# -----------------------------------------------------------------------------------
+
+# Pull the command out of the hook's stdin JSON (jq if available, else python3).
+if command -v jq >/dev/null 2>&1; then
+ cmd="$(jq -r '.tool_input.command // ""' 2>/dev/null)"
+else
+ cmd="$(python3 -c 'import sys, json; print(json.load(sys.stdin).get("tool_input", {}).get("command", ""))' 2>/dev/null)"
+fi
+
+# Couldn't read the command → stay neutral, let normal permission rules decide.
+[ -z "$cmd" ] && exit 0
+
+# A command word counts as "at a command position" after start-of-line, whitespace,
+# ; & | ( or a quote — and ends before whitespace, a quote, ) ; & | or end-of-line —
+# so `bash -c 'git push'` is still seen.
+b="(^|[[:space:];&|(\"'\`])"
+e="([[:space:];&|)\"'\`]|$)"
+# Global options that may sit between a tool and its subcommand (git -C dir push).
+opts='([[:space:]]+(-[A-Za-z-]+|[-_A-Za-z0-9]+=[^[:space:]]+|-C[[:space:]]+[^[:space:]]+))*'
+
+re="${b}(${RISKY_WORDS})${e}"
+# git push / git clean, docker push — allowing global options before the subcommand.
+re="$re|${b}git${opts}[[:space:]]+(push|clean)${e}"
+re="$re|${b}docker${opts}[[:space:]]+push${e}"
+# recursive rm (-r / -R / -fr / --recursive), with flags/paths in any order.
+re="$re|${b}rm([[:space:]]+[^[:space:]]+)*[[:space:]]+(-[A-Za-z]*[rR]|--recursive)"
+# find … -delete — irreversible bulk delete.
+re="$re|${b}find([[:space:]]+[^[:space:]]+)*[[:space:]]+-delete${e}"
+# package publishing — ships artifacts to a registry.
+re="$re|${b}(npm|pnpm|yarn|bun|cargo|gem)([[:space:]]+[^[:space:]]+)*[[:space:]]+publish${e}"
+re="$re|${b}twine([[:space:]]+[^[:space:]]+)*[[:space:]]+upload${e}"
+# gh mutations that merge, ship, or destroy.
+re="$re|${b}gh[[:space:]]+(pr[[:space:]]+merge|repo[[:space:]]+delete|release[[:space:]]+(create|delete))${e}"
+re="$re|${b}gh[[:space:]]+api([[:space:]]+[^[:space:]]+)*[[:space:]]+(-X|--method)[[:space:]]+(POST|PUT|PATCH|DELETE)${e}"
+# a deploy script invoked by path, e.g. ./scripts/deploy.sh — a word-list can't see it.
+re="$re|${b}([./A-Za-z0-9_-]*/)?deploy(\.[A-Za-z]+)?${e}"
+# WorkTable data migrations and endpoint regeneration rewrite committed artifacts.
+re="$re|${b}([./A-Za-z0-9_-]*/)?regenerate_endpoints(\.[A-Za-z]+)?${e}"
+
+if printf '%s\n' "$cmd" | grep -Eq "$re"; then
+ printf '%s' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"Prod-affecting / destructive command — confirm before running."}}'
+fi
+# No match → no output: fall through to the normal permission flow.
+exit 0
diff --git a/.claude/settings.json b/.claude/settings.json
new file mode 100644
index 0000000..a7af469
--- /dev/null
+++ b/.claude/settings.json
@@ -0,0 +1,44 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(cargo build:*)",
+ "Bash(cargo check:*)",
+ "Bash(cargo test:*)",
+ "Bash(cargo fmt:*)",
+ "Bash(cargo clippy:*)",
+ "Bash(cargo tree:*)",
+ "Bash(git status:*)",
+ "Bash(git diff:*)",
+ "Bash(git log:*)"
+ ],
+ "ask": [
+ "Bash(git push:*)",
+ "Bash(cargo publish:*)",
+ "Bash(npm publish:*)",
+ "Bash(bun publish:*)",
+ "Bash(docker push:*)",
+ "Bash(gh pr merge:*)",
+ "Bash(gh release:*)",
+ "Bash(aws:*)",
+ "Bash(gcloud:*)",
+ "Bash(az:*)",
+ "Bash(kubectl:*)",
+ "Bash(helm:*)",
+ "Bash(terraform:*)",
+ "Bash(flyctl:*)"
+ ]
+ },
+ "hooks": {
+ "PreToolUse": [
+ {
+ "matcher": "Bash",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/ask-before-risky-commands.sh\""
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/.gitignore b/.gitignore
index c4e641a..ad2e116 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,4 +9,3 @@ examples/minimal_project/config/generated/*
.vscode
.idea/
-CLAUDE.md
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..e151fbf
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,88 @@
+# Working agreement — endpointgen
+
+The operating contract for **any** coding agent working in this repository. This file is
+the single source of truth for the rules: Codex, Cursor and Gemini CLI read `AGENTS.md`
+natively, and Claude Code loads it through the `@AGENTS.md` import in
+[`CLAUDE.md`](CLAUDE.md). **Never fork these rules into a per-vendor file.**
+
+**Rust binary** (`endpoint-gen`).
+
+## Invariants (don't break these)
+
+- **Keep `cargo fmt` and `cargo clippy --all-targets` clean.** Lint failures are part of the build here, not advisory.
+- **Publishing to crates.io is irreversible.** A version number can never be reused, and yanking does not delete. Run `cargo publish --dry-run` first, publish from the merged default branch, and tag the release.
+- **A pre-release version (`-alpha`, `-beta`) needs an exact dependency pin.** A plain `"2.0"` requirement will not match `2.0.0-alpha.1`, so consumers must be bumped deliberately.
+- **Docs describe what is true now.** If you change behaviour, update the README and any affected doc in the same change.
+
+## Build & test
+
+```bash
+cargo build
+cargo test
+cargo fmt && cargo clippy --all-targets # run after every change
+```
+
+## Verification
+
+Run what you build before reporting it done. Type-checks and tests verify code correctness,
+not feature correctness — **if you can't run it, say so explicitly** rather than implying
+success.
+
+- Compare against the base branch rather than asserting: a pre-existing failing test or lint
+ error is not something you introduced, and saying so requires checking.
+- A build that finishes suspiciously fast was cached, not rebuilt. Force a real rebuild when
+ the rebuild is the thing you're verifying.
+
+## PR discipline
+
+**Always paste the full PR URL** (`https://github.com/pathscale/endpointgen/pull/`), not just the number, so it's
+clickable.
+
+
+
+## Keeping docs honest
+
+Hit a factual error here — a stale path, a wrong command, a moved status? Fix it in the same
+change. Don't open cosmetic rewording PRs.
+
+Learned something durable — a gotcha, a decision, a constraint? It belongs **in this repo's
+docs**, not in your agent's private memory. Repo docs are versioned, reviewable, and visible
+to every agent and human; private memory dies with your machine.
+
+## Git workflow
+
+- **Always specify the branch when pushing**: `git push origin branch-name`
+- **Branch naming**: `fix/issue-description` or `feat/issue-description`
+- **Force-push your own branch freely.** Rebasing a feature branch onto a moved
+ base, or amending before review, is normal and correct — use
+ `--force-with-lease` so you don't clobber someone else's push.
+- **Never force-push the default branch** (`main`/`master`). That is the history
+ everyone else builds on, and it is protected server-side for a reason.
+
+## Guardrails
+
+[`.claude/settings.json`](.claude/settings.json) and [`.claude/hooks/`](.claude/hooks/) make
+Claude Code prompt a human before prod-affecting or destructive commands — pushes, publishing
+to a registry, `gh pr merge`, cloud CLIs, recursive deletes, deploy scripts.
+
+**Other agents don't get that net automatically.** Apply the same rule yourself: ask before
+running any command family listed in
+[`.claude/hooks/ask-before-risky-commands.sh`](.claude/hooks/ask-before-risky-commands.sh).
+It is one layer of defence, not a guarantee — a pattern match over a command string is
+best-effort.
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..63abaf9
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,12 @@
+@AGENTS.md
+
+# Claude Code notes — endpointgen
+
+The import above is binding: [`AGENTS.md`](AGENTS.md) is the **working agreement** for this
+repository, and every Claude Code session loads it automatically. Don't copy rules here —
+one source of truth, no drift. Only genuinely Claude-specific wiring belongs below.
+
+- Guardrails live in [`.claude/settings.json`](.claude/settings.json): safe read-only
+ commands are pre-allowed; pushes, publishing, `gh pr merge`, cloud CLIs and deploys prompt
+ first (`permissions.ask` plus the `PreToolUse` hook in [`.claude/hooks/`](.claude/hooks/)).
+- Keep the hook's `RISKY_WORDS` and `permissions.ask` **in sync** — they back each other up.