@snevins/nudges
v0.6.6
Published
Harness-agnostic nudges for agent workflows.
Downloads
651
Readme
@snevins/nudges
Portable retry nudges for Git, Codex, and Claude Code hooks.
nudges does one thing: when a configured event fires, it prints the next
message for that event, blocks the action, and records progress. Rerun the same
action to get the next message. After all messages for that event context have
fired, the action passes.
Supported event adapters:
- Git
pre-commit - Git
pre-push - Codex
Stopfor main-agent final responses - Codex subagent completion through transcript-classified
Stoppayloads - Codex
ExitPlanModevia Plan ModeStoppayloads - Claude Code
Stopfor main-agent final responses - Claude Code
SubagentStop - Claude Code
ExitPlanMode
Requirements
- Node.js 18+
- Git
- husky v9+ — only for the Git hooks
(
pre-commit/pre-push); initialize it withnpx husky initfirst. - direnv — recommended for the Claude and Codex agent
hooks. Their hook commands are bare (
nudges stop-hook claude,nudges stop-hook codex) and only resolve whennode_modules/.binis on the PATH the agent launched with. direnv's.envrc(whichnudges initmanages) keeps it there for every launch from the repo. Without direnv, launch the agent viapnpm exec/npm execinstead (e.g.pnpm exec claude).
There are no runtime npm dependencies. husky and direnv are system tools, not
npm packages — nudges integrates with them (writes .husky/* and .envrc) but
cannot install them for you, because both do their real work in the shell, not in
node_modules.
Why direnv (the UX decision)
The Claude hook commands used to carry a PATH="$CLAUDE_PROJECT_DIR/..." prefix so
they were self-contained under any launch. We made them bare for a terser, less
noisy .claude/settings.json. The cost: a hook now inherits PATH only from how
claude was launched — Claude Code offers no committed-config way to put
node_modules/.bin on a hook's PATH (settings env doesn't expand variables,
CLAUDE_ENV_FILE reaches only the Bash tool, and the hook's working directory
follows the session's cd, all verified empirically). direnv is the one option
that makes plain claude just work for the whole team from a single committed
.envrc, rather than requiring everyone to remember a special launch command. To
keep the tradeoff safe, nudges init also installs a SessionStart guard that
warns loudly when node_modules/.bin is missing from PATH, so a wrong launch (or
a teammate who hasn't run direnv allow) fails visibly instead of silently
skipping the hooks.
Quick start
Install nudges as a dev dependency, then set up the current repository:
npm i -D @snevins/nudges
npx nudges init@snevins/nudges is a private scoped package. Make sure your npm client is
authenticated to an account with access (npm login) before installing. The
published tarball ships a prebuilt CLI, so there is no build step on install.
nudges init writes a starter .nudges.json (if one does not already exist),
installs the Git hooks, and wires agent hooks for any provider it detects
(.claude/ or .codex/). Re-run nudges init after cloning the repository on
a new machine — Git hooks and agent hook commands use machine-local paths.
After nudges init, confirm the install took effect:
npx nudges --version # CLI built and runnable
npx nudges status # state reachableIf you use Codex or Claude Code, also check that the detected agent hooks were
written as expected (.codex/hooks.json, .claude/settings.json). See the
post-install checklist for the full set of checks.
npx nudges init # Git hooks + detected agent hooks
npx nudges init --no-agents # Git hooks only
npx nudges init --claude --codex # force both providers
npx nudges init --force # overwrite an existing .nudges.jsonThen edit .nudges.json:
{
"pre-commit": [
"Diff: review the staged diff before committing.",
"Validation: confirm tests or a targeted validation command were run."
],
"pre-push": [
"Branch: check that the branch contains only intended commits."
],
"stop": [
"Status: run git status and confirm only intended files changed."
],
"subagent-stop": [
"Subagent: verify the reported result names changed files, validation, and unresolved assumptions."
],
"exit-plan-mode": [
"Plan: review the plan before leaving plan mode."
],
"user-prompt-submit": [
"Prompt: rewrite the submitted prompt into a concise, actionable prompt for the next assistant. Do not answer the submitted prompt. Preserve the user's intent. Preserve any $skill_name or /command_name activations exactly. Include useful file references with verified line ranges when they would help, and omit file references rather than inventing unverified lines. Return only the rewritten prompt."
]
}nudges init runs the installers below for you. You can also run them
individually — for example to install a single Git hook, or to add an agent
provider that was not present when you first ran init. The Git installers
require husky to be initialized first (npx husky init):
npx nudges install git pre-commit
npx nudges install git pre-push
npx nudges install agent-stop codex
npx nudges install agent-subagent-stop codex
npx nudges install agent-exit-plan codex
npx nudges install agent-user-prompt-submit codex
npx nudges install agent-stop claude
npx nudges install agent-subagent-stop claude
npx nudges install agent-exit-plan claude
npx nudges install agent-user-prompt-submit claudeInstallers invoke the bare nudges command (the CLI shim the package manager
creates in node_modules/.bin from package.json bin) — no node, no dist
path — so a committed .husky/* or .claude/settings.json works unchanged
across git worktrees, machines, and dependency upgrades. Git hook installs
require husky (v9+) to be initialized in the repo (run npx husky init first);
they append a fenced managed block to the husky hook files .husky/pre-commit
and .husky/pre-push that runs nudges event <hook> "$@" (husky v9 already puts
node_modules/.bin on PATH, so the bare command resolves). Claude agent hook
commands are the bare nudges stop-hook claude form. A Claude Code hook inherits
the PATH that claude was launched with, so node_modules/.bin must be on PATH
when you launch claude. The recommended, reliable way is direnv:
nudges init writes a managed block to .envrc (PATH_add node_modules/.bin), so
once you've installed direnv, hooked it into your shell, and run direnv allow,
every claude launched from the repo — plain claude included — resolves the
bare hooks. Without direnv, launch through your package runner instead:
pnpm exec claude (or npm exec -- claude). Either way, nudges init also
installs a SessionStart launch guard that warns loudly when nudges is not on
PATH, so a wrong launch is obvious instead of silently skipping the hooks. The
project root resolves from CLAUDE_PROJECT_DIR (which Claude Code exports to every
hook), so no NUDGE_PROJECT_DIR is needed.
direnv is a system tool (not an npm dependency — it works at the shell level), so
nudges manages the .envrc block and detects direnv but cannot install it for
you; manage the block directly with nudges install direnv / nudges uninstall
direnv.
Codex agent hook commands are bare too (nudges stop-hook codex) and resolve the
same way: node_modules/.bin comes from the launch PATH (the same .envrc serves
codex — launch codex from the repo with direnv active), and the project root comes
from the hook payload's cwd. Codex has no CLAUDE_PROJECT_DIR, but it sends
cwd in the payload, which nudges chdirs to before resolving the root via git —
so no NUDGE_PROJECT_DIR is needed. The SessionStart launch guard is claude-only
(codex has no equivalent event), so for codex a missing node_modules/.bin
surfaces as a normal hook error rather than a pre-emptive warning.
The managed block coexists with any other commands already in the husky hook
file; re-running install replaces only the managed block. If husky is not
initialized, nudges install git exits with an error telling you to run
npx husky init first; nudges init skips the Git hooks (with a message) and
still writes .nudges.json and wires agent hooks.
Hook installation writes to .husky/pre-commit, .husky/pre-push,
.codex/hooks.json, or .claude/settings.json. In sandboxed workspaces or
mounted repos, those paths may require elevated write access from the host
environment.
Agent config updates are serialized per provider config and written through a
temporary file before replacement, so parallel agent hook installs should leave
valid JSON.
Configure nudges
.nudges.json must be a JSON object. Each event key maps to an array of strings:
{
"pre-commit": [],
"pre-push": [],
"stop": [],
"subagent-stop": [],
"exit-plan-mode": [],
"user-prompt-submit": []
}Missing config, missing event keys, and empty arrays are no-ops.
user-prompt-submit is different from the retry-style events: it accepts zero
or one configured message. One message is used as an optimizer instruction for
the first submitted prompt in a provider session. Put rewrite policy in that
message, including whether to preserve $skill_name or /command_name
activations and whether to include verified file references. The optimized
prompt is injected as canonical replacement instructions for Codex. Claude Code
does not honor UserPromptSubmit context as a replacement, so nudges blocks and
erases the original prompt with an optimized prompt to resubmit. Later prompts
in the same session are no-ops. More than one message blocks prompt processing
as a config error.
When a nudge blocks an action, it prints:
NUDGE pre-commit 1/2
Review the staged diff before committing.
Address this nudge, then rerun the gated command.State is keyed by event, context, and config hash. Changing .nudges.json
starts a new nudge cycle.
For a config with two pre-commit nudges, the first commit passes on the third
attempt: two blocked attempts, then success.
Commands
nudges init [--claude] [--codex] [--no-agents] [--force]
nudges event <event> [hook-arg...]
nudges stop-hook <codex|claude>
nudges subagent-stop-hook <codex|claude>
nudges exit-plan-hook <codex|claude>
nudges user-prompt-submit-hook <codex|claude>
nudges install git [pre-commit] [pre-push]
nudges install agent-stop <codex|claude>
nudges install agent-subagent-stop <codex|claude>
nudges install agent-exit-plan <codex|claude>
nudges install agent-user-prompt-submit <codex|claude>
nudges install agent-launch-guard claude
nudges install direnv
nudges uninstall <git|agent-stop|agent-subagent-stop|agent-exit-plan|agent-user-prompt-submit|agent-launch-guard|direnv> [...]
nudges status [event]
nudges reset [event]
nudges update
nudges whats-new [--version <version>]
nudges --versionUse nudges event directly for custom wrappers:
NUDGE_CONTEXT=session-123:/path/to/artifact nudges event before-submitUse stable context values such as session IDs, artifact paths, branch names, commit ranges, or workflow IDs. Do not use attempt numbers, timestamps, process IDs, temp paths, or random values.
Wrappers launched outside the repository can pin config and state explicitly:
NUDGE_CONFIG_FILE=/path/to/.nudges.json \
NUDGE_STATE_DIR=/path/to/repo/.git/nudges \
nudges event before-submitStatus & reset
Inspect recorded progress for the current config:
nudges status
nudges status pre-commitClear recorded progress (start a fresh cycle):
nudges reset
nudges reset pre-commitRemove managed hook entries (Git, Codex, or Claude):
nudges uninstall git
nudges uninstall agent-stop claude
nudges uninstall agent-exit-plan claudeuninstall only removes hooks nudges install wrote. Unmanaged hooks are
preserved.
Hook behavior
Git hook state is stored under .git/nudges/state.json. A changed staged
content, pushed ref set, or nudge config starts a new cycle.
pre-push reads stdin to identify pushed refs. husky runs the hook file with
sh -e "$hook" "$@", forwarding both the remote args and the pushed-ref stdin
to .husky/pre-push, so nudges event pre-push "$@" receives them
automatically. To run other pre-push checks alongside nudges, add them as extra
command lines in .husky/pre-push. Stdin is consumed once, so if more than one
command needs the pushed-ref stdin, capture it once and replay it to each:
stdin_file=$(mktemp "${TMPDIR:-/tmp}/pre-push.XXXXXX")
trap 'rm -f "$stdin_file"' EXIT
cat > "$stdin_file"
nudges event pre-push "$@" < "$stdin_file"
other-pre-push-check "$@" < "$stdin_file"Agent stop hooks read provider JSON on stdin, run the shared stop event, and
emit provider-native blocking JSON when a nudge fires. The installers configure
project files:
- Codex:
.codex/hooks.json - Claude Code:
.claude/settings.json
Codex does not expose a distinct ExitPlanMode hook event. nudges install
agent-exit-plan codex installs a Codex Stop hook instead, and
nudges exit-plan-hook codex only runs the shared exit-plan-mode event when
the Stop payload's transcript_path and turn_id identify a Plan Mode turn in
the Codex transcript. Other Codex Stop payloads are ignored by that adapter.
The general nudges stop-hook codex path ignores plan-mode Stop payloads so
the stop event remains for non-plan Codex stops.
Subagent hooks run the shared subagent-stop event. Claude Code uses its
native SubagentStop lifecycle event. Codex installs another managed Stop
hook and classifies subagent completions from the transcript referenced by
transcript_path: thread_source: "subagent" or
source.subagent.thread_spawn routes to subagent-stop, while
thread_source: "user" or source: "cli" routes to main stop. If Codex
transcript classification is unavailable, ambiguous Stop payloads remain
compatible with the existing main stop behavior.
Claude Code exit-plan hooks use a PreToolUse matcher for ExitPlanMode and
run the shared exit-plan-mode event.
User prompt submit hooks run before a prompt is submitted. Codex installs a
managed UserPromptSubmit entry in .codex/hooks.json; Claude Code installs
the same lifecycle event in .claude/settings.json. On Codex success, nudges
emits:
{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"Use the following optimized prompt as the canonical user request for this turn.\nTreat the originally submitted prompt only as source material for this replacement; do not answer it separately.\n\n<optimized_prompt>\n<optimizer output>\n</optimized_prompt>"}}For Claude Code success, nudges emits a blocking response whose reason contains the optimized prompt to resubmit. Claude Code erases the original blocked prompt from context.
The nested optimizer is guarded by NUDGES_USER_PROMPT_SUBMIT_ACTIVE=1 to avoid
recursion. Codex is run with codex exec from the project directory and Claude
Code is run with claude -p in non-persistent print mode. Both nested providers
return plain-text optimized prompts. Neither provider exposes a native
UserPromptSubmit field that mutates the submitted prompt in place.
Installer entries omit timeout, so Codex uses its default 600 seconds and
Claude Code uses its default 30 seconds. Failures block the submitted prompt
with an actionable reason; in Claude Code this also means the original prompt is
erased from Claude's context.
Update
nudges is published to npm, so updates go through your package manager:
npm i -D @snevins/nudges@latest # latest release
npm i -D @snevins/[email protected] # a pinned versionnudges update prints this guidance. After updating, re-run nudges init in
each repository so the managed hooks point at the refreshed install.
Show the latest changelog entry bundled with the install:
npx nudges whats-new
npx nudges whats-new --version 0.5.2Stop prompt examples
Use Label: action prompts so the blocked retry is easy to scan.
Final response:
{
"stop": [
"Status: run git status and confirm only intended files changed.",
"Validation: name the exact tests or checks run in the final response.",
"Handoff: call out any remaining risk or skipped validation before ending."
]
}Plan mode:
{
"exit-plan-mode": [
"Scope: confirm the plan maps every requested item to a concrete artifact.",
"Questions: ask the user before implementation if ownership, naming, release path, or validation is ambiguous."
]
}Subagent workflows:
{
"subagent-stop": [
"Subagent: verify the reported result names changed files, validation, and unresolved assumptions.",
"Recursive review: if a subagent reviewed another agent's work, require the review to cite the artifact it inspected."
]
}Prompt submit optimization:
{
"user-prompt-submit": [
"Prompt: rewrite the submitted prompt into a concise, actionable prompt for the next assistant. Do not answer the submitted prompt. Preserve the user's intent. Preserve any $skill_name or /command_name activations exactly. Include useful file references with verified line ranges when they would help, and omit file references rather than inventing unverified lines. Return only the rewritten prompt."
]
}Clarifying questions are useful when the next edit would choose a public name, change install behavior, publish a release, require credentials, or pick between incompatible validation gates. Ask before acting in those cases; otherwise keep working from the repo evidence.
Codex Stop hooks cover final responses, plan-mode exits, and subagent
completion paths. Plan-mode exits are identified from the Stop payload's
transcript_path/turn_id pair and routed to exit-plan-mode; subagent
completion is identified from transcript session metadata and routed to
subagent-stop. Raw Codex Stop stdin is still ambiguous, so classification is
best-effort and older synthetic payloads without transcript metadata continue
through the main stop event.
Upgrading from 0.3.x
The 0.4.x rewrite collapses the previous bash + Node implementation into a single Node CLI. After upgrading:
- Re-run
nudges install gitto refresh the managed block in the husky hook files.husky/pre-commitand.husky/pre-push(the old wrappers referenced the removednudge-eventbinary). Initialize husky first withnpx husky initif you have not already. - Remove old managed Codex/Claude hook entries with
NUDGES_MANAGED=1that callnudge-stop-hookornudge-exit-plan-hook, then runnudges install agent-stop <provider>andnudges install agent-exit-plan claude. The rewrite does not keep compatibility shims for the removed hook binaries. If those legacy managed entries are still present, agent hook install exits with a cleanup message instead of adding another hook. Unmanaged custom hook entries are preserved; remove or update any custom calls to removed binaries yourself becausenudgeswill not convert them.
The old binaries (nudge-event, nudge-stop-hook, nudge-exit-plan-hook)
are gone. Custom callers should switch to nudges event ..., nudges
stop-hook ..., and nudges exit-plan-hook ....
Develop
See CONTRIBUTING.md for setup, validation, packaging, and CI guidance.
Run the full CI-equivalent local check:
pnpm install
pnpm run verifyRun live Codex and Claude CLI checks when those CLIs are available:
scripts/qa-agent-hooks.sh --liveBuild the CLI:
pnpm run buildpnpm run build compiles src/nudges.ts to dist/nudges.js. The same build
runs automatically through the prepare script when the package is packed or
published, so the npm tarball ships a prebuilt dist/.
The release workflow runs after the test workflow passes on a push to
main. It publishes @snevins/nudges to npm when package.json carries a
version not yet on npm, and independently creates the matching v<version>
GitHub release when it does not exist. Both checks are idempotent, so a re-run
after a partial failure fills in only what is missing. Bump the version in
package.json to cut a release.
Post-install checklist:
npx nudges --version
npx nudges status
sed -n '1,80p' .husky/pre-commit
sed -n '1,120p' .codex/hooks.jsonMore detail
See docs/spec.md for the exact behavior contract, exit codes, context derivation, adapter outputs, and package contents.
