Skip to content

Commit 09c6624

Browse files
DeusDataabendrothj
andcommitted
perf(hook-augment): gate no-op Bash calls before identity hashing
Maintainer commit on top of the Bash-matcher work, closing the cost question the 2026-08-03 review left open before default-on shipped. Measured on a production binary (2026-08-28): every hook invocation paid ~1.1-1.2 s of user CPU BEFORE any gating — not in cbm_mcp_server_new as first suspected, but in main()'s executable-identity hash for cohort admission, which the hook-client path runs ahead of any input inspection. With Bash in the installed matcher, nearly every agent command (`ls`, `make`, `git`, ...) would have paid that for nothing: agents issue far more Bash calls than Grep/Glob, and almost none are searches. The fix: main() now reads stdin on the hook-client path and exits before identity hashing when the event can never produce output — an un-forced PreToolUse Bash event whose command yields no search pattern or no queryable token. A no-op touches no shared state, so it needs no identity. The gate is a pure exported function using the exact parser pair ha_process uses, so gate and augmenter cannot disagree; anything uncertain (lifecycle events, other tools, coverage adapters, oversized stdin, forced events) pays full fare unchanged. Consumed stdin is handed back through a prefetch so downstream readers see it exactly once. Measured after: no-op Bash calls ~10 ms wall / 0.00 s user CPU (was ~1.2 s); real Bash searches and Grep/Glob unchanged at full fare. cli 259/0, mcp 179/0. Co-authored-by: Jake Abendroth <contact@jakea.net> Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
1 parent 36b7b18 commit 09c6624

3 files changed

Lines changed: 75 additions & 0 deletions

File tree

‎src/cli/cli.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,10 @@ char *cbm_hook_augment_lifecycle_json_for(const char *input, const char *forced_
477477
* hard fail-open deadline without constructing a local MCP/store instance. */
478478
void cbm_hook_augment_arm_deadline(void);
479479
char *cbm_hook_augment_read_stdin(void);
480+
/* Pure no-op gate for the hook-client fast path (see hook_augment.c). */
481+
bool cbm_hook_augment_input_is_noop_bash(const char *input);
482+
/* Hand back stdin bytes main() consumed early; the next read returns them. */
483+
void cbm_hook_augment_prefetch_stdin(char *owned);
480484

481485
/* Process one already-read hook payload using a caller-owned MCP session.
482486
* Returns a malloc-owned hook output JSON string, or NULL for fail-open/no

‎src/cli/hook_augment.c‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,22 @@ void cbm_hook_augment_arm_deadline(void) {
154154

155155
/* ── stdin ────────────────────────────────────────────────────────── */
156156

157+
/* main() reads stdin EARLY on the hook-client path so the no-op gate can run
158+
* before executable-identity hashing; the consumed bytes are handed back here
159+
* so every downstream reader sees them exactly once. */
160+
static char *g_ha_prefetched_stdin = NULL;
161+
162+
void cbm_hook_augment_prefetch_stdin(char *owned) {
163+
free(g_ha_prefetched_stdin);
164+
g_ha_prefetched_stdin = owned;
165+
}
166+
157167
char *cbm_hook_augment_read_stdin(void) {
168+
if (g_ha_prefetched_stdin) {
169+
char *out = g_ha_prefetched_stdin;
170+
g_ha_prefetched_stdin = NULL;
171+
return out;
172+
}
158173
char *buf = malloc(HA_STDIN_CAP + 1);
159174
if (!buf) {
160175
return NULL;
@@ -1617,6 +1632,40 @@ char *cbm_hook_augment_process_for(cbm_mcp_server_t *srv, const char *input_json
16171632
return ha_process(srv, input_json, forced_event, dialect);
16181633
}
16191634

1635+
/* TRUE iff `input` is an un-forced PreToolUse Bash event whose command can
1636+
* never produce augmentation output: not a recognised search, or no queryable
1637+
* token. Pure string work — safe to run BEFORE executable-identity hashing,
1638+
* cohort admission, or any daemon contact, which is the point: agents issue
1639+
* far more Bash calls than Grep/Glob and nearly all are not searches, while
1640+
* the identity hash alone costs ~1.1 s of user CPU per invocation on a
1641+
* production binary (measured 2026-08-28). Uses the exact parser pair
1642+
* ha_process uses, so the gate and the augmenter cannot disagree about what
1643+
* counts as a search. Anything uncertain returns false and pays full fare:
1644+
* lifecycle events, other tools, coverage adapters, oversized stdin. */
1645+
bool cbm_hook_augment_input_is_noop_bash(const char *input) {
1646+
if (!input || strlen(input) > HA_STDIN_CAP) {
1647+
return false;
1648+
}
1649+
yyjson_doc *doc = yyjson_read(input, strlen(input), 0);
1650+
if (!doc) {
1651+
return false;
1652+
}
1653+
bool noop = false;
1654+
yyjson_val *root = yyjson_doc_get_root(doc);
1655+
const char *event = ha_hook_event_name(root);
1656+
const char *tool = ha_obj_str(root, "tool_name");
1657+
if (event && tool && strcmp(event, "PreToolUse") == 0 && strcmp(tool, "Bash") == 0) {
1658+
yyjson_val *tin = yyjson_obj_get(root, "tool_input");
1659+
const char *cmd = ha_obj_str(tin, "command");
1660+
char pat[HA_BASH_TOK_SZ];
1661+
char tok[HA_MAX_TOKEN + 1];
1662+
noop = !ha_parse_bash_search_pattern(cmd, pat, sizeof(pat)) ||
1663+
!ha_extract_token(pat, tok, sizeof(tok));
1664+
}
1665+
yyjson_doc_free(doc);
1666+
return noop;
1667+
}
1668+
16201669
int cbm_cmd_hook_augment(int argc, char **argv) {
16211670
cbm_hook_augment_arm_deadline();
16221671

@@ -1642,6 +1691,10 @@ int cbm_cmd_hook_augment(int argc, char **argv) {
16421691
if (!input) {
16431692
return 0;
16441693
}
1694+
if (!forced_event && cbm_hook_augment_input_is_noop_bash(input)) {
1695+
free(input);
1696+
return 0;
1697+
}
16451698
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
16461699
char *output = srv ? ha_process(srv, input, forced_event, dialect) : NULL;
16471700
if (output) {

‎src/main.c‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1791,6 +1791,24 @@ int main(int argc, char **argv) {
17911791
#ifndef _WIN32
17921792
cbm_hook_augment_arm_deadline();
17931793
#endif
1794+
/* Read stdin now and bail before executable-identity hashing when the
1795+
* event can never produce output (an un-forced PreToolUse Bash call
1796+
* that is not a search). The identity hash costs ~1.1 s of user CPU
1797+
* per invocation, and with Bash in the installed matcher nearly every
1798+
* agent command would pay it for nothing. A no-op touches no shared
1799+
* state, so it needs no identity. Anything else is handed back via
1800+
* the prefetch so downstream readers see stdin exactly once. */
1801+
if (!hook_event) {
1802+
char *hook_input = cbm_hook_augment_read_stdin();
1803+
if (!hook_input) {
1804+
return EXIT_SUCCESS; /* fail open, nothing to augment */
1805+
}
1806+
if (cbm_hook_augment_input_is_noop_bash(hook_input)) {
1807+
free(hook_input);
1808+
return EXIT_SUCCESS;
1809+
}
1810+
cbm_hook_augment_prefetch_stdin(hook_input);
1811+
}
17941812
}
17951813

17961814
if (role == CBM_DAEMON_PROCESS_STATELESS) {

0 commit comments

Comments
 (0)