cd /news/ai-agents/a-hook-should-be-a-function-call-not… · home › topics › ai-agents › article
[ARTICLE · art-154823] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=↑ positive

A hook should be a function call, not a shell

A developer moved thirteen of the shell-script hooks in workkit, a workflow kit for Claude Code, into a single plugin module loaded once per session, cutting each hook fire from about 90 ms at the median to near zero on an M1 Max. The same update routes the agent's GitHub bookkeeping through a workkit-agent[bot] GitHub App and replaces assignee-based claims with an agent:claimed label. A separate fix in the omega framework caps the Firebase emulator's auth-trigger worker pool with a mapLimited helper after a 91-user seed hit 52 workers and ran out of memory on a 7 GB CI runner.

by read4 min views4 publishedOct 21, 2026

This one covers October 14 to 21. Most of the week went into one idea: the small scripts that guard my Claude Code sessions should not each start a shell. The rest went into giving the agent its own name on GitHub, and into a framework bug that only showed up on a small CI runner.

workkit is my workflow kit for Claude Code: GitHub issues as the work list, labels as the pipeline, and hooks that enforce the rules. A hook is a small program Claude Code runs on an event, like a prompt, a file edit, or a shell command. It can add context, warn, or block the action.

Until this week every hook was a shell script. Each fire went through a script, which started bash, which usually called jq to read the event, and often node for the real logic. That is three or four processes for one check.

The count adds up fast. Before version 0.79, every prompt I sent started four shell hooks. Every file edit started four more after the write. Session start ran four, and every subagent spawn ran three.

So I timed it. On my M1 Max, with other work running and the machine busy, one fire routed through the took about 90 ms at the median. A bare bash, jq and node chain alone took about 38 ms. A function call inside a process that is already running rounds to zero.

Claude Code can now load a plugin module once per session and call it on each event. workkit 0.79 moved thirteen hooks into one such module, and fifteen live there now. My personal hooks did the same in my dotfiles: seven of them, including the ones that stamp the time and check each reply's format, run as one module.

The design rule that made the move safe: only one file, register.js, touches the host's interface. It gathers the facts a hook needs (a file's text, a git answer, a stored marker), and hands them to plain functions in hooks/mods/<name>.js. Those functions take plain data and return plain data. So the logic tests just require() them and need no Claude Code at all. Only a thin set of wiring tests runs through claude plugin test.

The hooks that guard shell commands, like the commit gate, stay shell hooks for now. They are next.

workkit has the agent do a lot of bookkeeping on GitHub: it claims an issue, parks it for my check with a comment, posts the proof, and closes it. Until 0.76 all of that posted as me. Reading an issue's history, I could not tell my decisions from the agent's notes.

Now that bookkeeping posts as workkit-agent[bot], a GitHub App. How the token gets there was the interesting part:

gh shim on the PATH. A call that writes (a comment, a label, a close) gets the bot's token. A read, a commit and a push still run as me. The claim also changed. It used to be the assignee field, which meant me. It is now an agent:claimed label, so the claim is the agent's, in plain sight.

omega is my framework for building a brand's website, backend, desktop app and browser extension from one config. Its backend self-test seeds 91 test users. It created them all at once.

Each new user fires an auth trigger, a function that runs on sign-up. The Firebase functions emulator starts one worker per trigger in flight, and it never caps that pool. On a 7 GB CI runner the test reached 52 workers and ran out of memory. A machine with more memory hides the problem.

The fix is a small helper, mapLimited, that runs the seed four at a time. The seed had a second bug too: it reported every user created even when the emulator had loaded no functions at all. A green seed was hiding a backend that never ran. Now a user whose sign-up trigger never finishes stops the seed and names the account.

Two more changes I like:

omega_ names. An old name does not quietly keep working: it throws, and the error names its replacement. omega migrate rewrites them for you.workkit log --follow tails it. It is the first place I look when a hook seems silent. The remaining shell hooks, the commit gate among them, are next in line for the module. If you write hooks for an AI coding tool, I would like to hear how you keep them fast. Comments are open.

── more in #ai-agents 4 stories · sorted by recency
── more on @workkit 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/a-hook-should-be-a-f…] indexed:0 read:4min 2026-10-21 · —