[BUG] session.compact mod hook no longer raised in 2.1.299 (manual and automatic compaction)
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Since 2.1.299, a mod's session.compact hook never runs. Neither /compact nor automatic compaction raises it, but the compaction itself still happens: the classic SessionStart hook with source: "compact" still fires when it ends. Other hooks in the same mod (turn.start, command.run) still run normally.
The 2.1.299 changelog doesn't mention any change to session.compact, so this looks like a regression.
Versions
- Broken: 2.1.299
- Last known working: 2.1.296. On that version
session.compactran before every manual and automatic compaction, withtriggerset tomanualorauto. - macOS (Darwin 25), terminal, fullscreen TUI
What Should Happen?
Expected (as on 2.1.296): a session.compact post with trigger: "manual" before the compaction.
Actual (2.1.299): the screen shows ⎿ Compacted (ctrl+o to see full summary), but the listener prints only:
listening on /tmp/compactprobe.sock
18:17:08 {"event":"turn.start"}
18:17:33 {"event":"command.run"}
turn.start comes from the prompt and command.run from /compact. session.compact never arrives.
Automatic compaction behaves the same way. Forcing it early with CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=5 compacted three times in one turn, with three classic SessionStart source=compact events and no session.compact.
Impact
Tools that read a session's state from hooks lose the start of a compaction. An automatic compaction inside a turn now shows up only as SessionStart source=compact, so the session looks idle while it is still in the middle of a turn. Mods that customise compaction through this hook (see #101879, #101096, #95328) would presumably stop running too.
Error Messages/Logs
Steps to Reproduce
Repro
A plugin with a hooks module that reports three events to a local unix socket:
.claude-plugin/plugin.json
{ "name": "compactprobe", "version": "0.0.1", "description": "probe" }
hooks/hooks.json
{ "modules": ["./register.js"] }
hooks/register.js
async function post($, data) {
await $.http.fetch('http://probe/', {
method: 'POST',
socketPath: '/tmp/compactprobe.sock',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(data),
})
}
export function register(on) {
on('turn.start', async ($, e, next) => { await post($, { event: 'turn.start' }); return next(e) })
on('command.run', async ($, e, next) => { await post($, { event: 'command.run' }); return next(e) })
on('session.compact', async ($, e, next) => { await post($, { event: 'session.compact', trigger: e.trigger }); return next(e) })
}
listen.mjs, a listener that prints each post and answers it:
import http from 'node:http'
import fs from 'node:fs'
const sock = '/tmp/compactprobe.sock'
fs.rmSync(sock, { force: true })
http.createServer((req, res) => {
let body = ''
req.on('data', (c) => (body += c))
req.on('end', () => { console.log(new Date().toISOString().slice(11, 19), body); res.end('ok') })
}).listen(sock, () => console.log('listening on', sock))
- In one terminal:
node listen.mjs - In another:
claude --plugin-dir <path-to-plugin> --model haiku - Send one prompt (
Reply with one word: hello), then run/compact.
Claude Model
Opus
Is this a regression?
Yes, this worked in a previous version
Last Working Version
2.1.296
Claude Code Version
2.1.299
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Terminal.app (macOS)
Additional Information
_No response_
3 Comments
There is an intentional user-tier bypass worth checking before classifying this as an event-emission regression.
In the public
sec-defaultimplementation at commit94506ff0d3bd9185f1752da36e968775021fb48f,session.compactcontinues directly to the append tier:The event policy says that user-tier compaction hooks do not run when this built-in is seated. It protects the resulting conversation and summary.
turn.*andcommand.runstill pass, so your other two probe events firing does not rule this out. Even a listener that only observessession.compactis skipped by this rule.The seating conditions include managed settings / managed MCP configuration, or a Team/Enterprise organization, subject to the managed prepend-plugin configuration.
Could you confirm whether
sec-default@builtinis active in the affected session, and whether the 2.1.296 and 2.1.299 runs used the same organization and managed configuration? A sanitized indication of the loaded built-ins would help; credentials and configuration contents are unnecessary.This is a source-level diagnostic, not an independent runtime reproduction. It does not establish why the behavior changed between those versions. If the built-in is absent, the engine's dispatch path still needs investigation; if it is present, distinguishing policy suppression from a missing event would make the diagnosis clearer.
Thanks, that was it.
sec-defaultis seated in the affected sessions, so this is the policy hold and not a dispatch regression.Seating. The account is in a Team/Enterprise organization with server-delivered managed settings. There is no managed settings file on the machine and no
prependPlugins. The 2.1.296 and 2.1.299 runs used the same machine and sign-in.Runtime check (2.1.300, same repro). I added user-tier hooks on two more events the README says
sec-defaultholds (settings.read,session.append), plus classicPreCompact/SessionStarthooks in the same plugin'shooks.json. One prompt, then/compact:The compaction happened, but the user hook received no
session.compact, nosession.appendfrom thecompactiondoor, and nosettings.readat all. Rows from theprompt,response,noticeandnotedoors still reached it. That matches the README's rows for a seatedsec-default.Why it changed between versions. The
session.compactrow (register.tsL58 and its README row) first appears inmods/in 3deb873687 (2026-10-11), after v2.1.296 (2026-10-09) and before v2.1.299 (2026-10-14). The seat didn't change; the hold arrived with it.What's left, if useful:
session.compacthook silently stops running, and that includes hooks that only observe.PreCompacthook still fires withtrigger, so we're moving our start-of-compaction signal to it.Happy to close this as intended behaviour if (2) isn't on the table. Thanks again for the help!
Thanks for checking the active built-ins and adding the 2.1.300 trace. That resolves the dispatch question for the tested configuration.
I independently checked the public source history:
v2.1.296has nosession.compacthold; 3deb873687 adds it, andv2.1.299line 58 contains it. The commit's parent is the 2.1.296 tag. This source comparison supports your version boundary; the runtime confirmation is yours.Your classic
PreCompactroute matches the public input contract for a manual/auto start signal. It should not be assumed to cover everysession.compacttrigger, which also includespluginandprecompute.For the remaining observation request, a separate lifecycle notification carrying
triggerandagentId, without the conversation or a replacement result, would be narrower than running the existing replacement hook and discarding its answer. Discarding a return value alone does not restrict the handler's API calls. The existing summary/conversation hold should remain intact; the dispatcher would need to provide the observation contract.A concrete release-note addition for maintainer consideration:
I have not independently rerun compaction or implemented a new observation event. Whether to add that event or change the release notes remains a maintainer decision.