Configure the Stop hook so Claude Code cannot finish a turn while your lint command fails, and hand the failure output back to the agent to fix.
pks hooksThe Stop handler is the one hook in pks that enforces something — or would, once it works. Configure a lint command, and when Claude Code tries to end a turn, pks is meant to run that command first — a non-zero exit blocks the stop and returns the output so the agent fixes the problem before finishing.
Known limitation. As of the current release, the lint command you set in the menu below is not written to disk — it only ever exists in the memory of the
pks hooksprocess that set it. Claude Code invokespks hooks stopas a fresh, separate process for every Stop event, and that new process reads its configuration from disk, so it never sees the command you configured. In practice the quality gate described on this page does not currently take effect. The rest of this page documents the intended workflow; the caveat in step 3 explains exactly where it breaks.
bash -c in the directory Claude Code operates in. Shell aliases and PATH entries that exist only in your interactive shell will not be there.pks hooks
Running pks hooks with no subcommand opens the configuration menu. It currently offers one area: quality — the lint check on stop.
The menu offers a preset list plus a free-text entry:
npm run lintdotnet build --no-restorenpx eslint .The choice is written to the configuration key hooks:quality:lint_command — but only in memory, for the lifetime of this pks hooks process. The underlying config service persists a setting to disk only when it's written with global scope or under a cli. prefixed key; hooks:quality:lint_command is neither, so nothing is saved to ~/.pks-cli/settings.json or any other file. The moment this process exits, the value is gone. The next pks hooks stop invocation — which is what Claude Code actually runs, as a brand-new process — reads that key back as unset and proceeds with no lint check. There is currently no supported way to make the choice survive across invocations.
If the current project's .claude/settings.json does not already reference pks hooks stop, the menu registers the Stop hook for you at that moment, using the same non-force logic as pks hooks init. That registration itself does persist (it's a direct file write, not a config-service setting) — it's only the lint command that fails to survive.
When Claude Code fires the Stop event, the handler runs in this order:
stop_hook_active: true, it proceeds immediately. That field is Claude Code's re-entrancy guard and prevents a stop loop.hooks:quality:lint_command. If it is unset, it proceeds with no action — and because the key never persists (see step 3 above), it is unset in the fresh process Claude Code spawns for every Stop event, even after you've "enabled" it in the menu.bash -c in the current working directory, capturing stdout and stderr together, with a 60-second timeout.A block tells Claude Code not to finish, and surfaces the lint output so the failure can be addressed in the same turn.
Simulate the event by piping an empty payload:
echo '{}' | pks hooks stop
echo | pks hooks stop is itself a new process, so — given the non-persistence caveat above — this will read hooks:quality:lint_command as unset and proceed silently even right after you "enabled" it in the menu, regardless of whether your lint command would pass or fail. A silent proceed here is not evidence the gate is working; it is currently the only outcome this command can produce.
Re-open pks hooks and choose the disable option. That deletes the in-memory configuration key for that process — moot in practice, since (per the caveat above) the key was never written to disk in the first place, so every fresh process already behaves as if it were disabled. It does not remove the Stop entry from settings.json, so pks hooks stop still runs on every stop — it just returns immediately because no lint command is configured. To remove the invocation entirely, delete the Stop entry from the settings file by hand.
Every stop is blocked, but the lint passes in my terminal. The command runs through bash -c, not your login shell. Aliases, shell functions, and PATH additions made in an interactive rc file are absent. Use an absolute path or a package-manager script.
Stops are blocked with a timeout message. The lint command exceeded 60 seconds and the process tree was killed. That is treated as failure. Narrow the command's scope.
The lint command is set but nothing runs. Two possible causes, check both. First and most likely: the lint command does not currently persist to disk (see the caveat in step 3), so any process other than the one that set it — including every real Stop event Claude Code fires — reads it as unset. Second: the Stop entry is missing from the settings.json that Claude Code actually loads. Run pks hooks init in the right scope and re-check the file; this fixes the second cause only.
Claude Code loops on stopping. The handler relies on stop_hook_active in the incoming payload to break the loop. A malformed payload is swallowed and treated as if the field were absent, which re-runs the lint. Confirm the lint command is deterministic and can actually be made to pass.
Stop entry gets into settings.json