Dev.to · 6 min read

A hook should be a function call, not a shell

A hook should be a function call, not a shell

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. Hooks that run inside Claude Code 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 loader 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 loader 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/.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. The agent signs its own work 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: The key never reaches my machine. A small hosted service holds the App's private key. It checks that the App is installed on the repo and that my sign-in can write to it. Then it hands back the bot's token for that one repo, good for one hour. A shim decides who is speaking. workkit puts a 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. Sign in once per machine. Setup runs GitHub's device flow: it prints a code, opens the page, and finishes once I approve. An agent can run it too and pass the code to 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: 91 sign-ups at once sank a CI runner 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: Brand Workers. A brand can ship its own Cloudflare Worker. Its script is looked up in the brand's folder first, then the company's, then the framework's. A missing script or secret fails before any request leaves. Renames that cannot rot. The old Jekyll-style template filters now carry omega_ names. An old name does not quietly keep working: it throws, and the error names its replacement. omega migrate rewrites them for you. Small but satisfying A hook log. Every hook fire now writes one line to a log: time, hook, event, session, folder, and whether it ran or stood down. workkit log --follow tails it. It is the first place I look when a hook seems silent. Findings stay in chat. The agent used to file an issue for every side finding. Now it lists them in its reply and files one only when I say so. My inbox is quieter. A size warning. My patched Claude Code footer shows a warning chip once a session's transcript passes 1 GB. Past that size, a session can fail to reopen. I learned that from a 2.3 GB one. Daily posts. The daily dev.to posts kept going: one open source project a day, studied and, where it is safe, run. 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.

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More AI & Machine Learning News