Problem
I run the same set of behavioral extensions across multiple coding agent harnesses (Claude Code, Pi, and DeepSeek TUI). The portability stack is simple: MCP for tools, hooks for behavioral extensions that inject context or react to agent lifecycle events. DeepSeek TUI has MCP, which is great. But its hook surface doesn't support the two events that matter most for portable extensions, so I had to build an SSE sidecar that spawns deepseek serve --http and owns the user-input path entirely, just to get the same extension behavior I get natively in other harnesses.
The sidecar works but it replaces the TUI. No slash commands, no status bar, no in-TUI MCP manager, no image rendering. The quality gap compared to running the same extensions as native Pi extensions or Claude Code hooks is noticeable.
What I need
1. message_submit with mutation rights (critical)
A hook that fires before the user's message is sent to the model, where the hook can modify the message text. This is the single most important missing piece.
Use case: I run a "subconscious memory" extension that, on every user submission, queries a knowledge base for relevant context from past conversations, then prepends it to the user's message inside XML tags. The agent sees the context as part of the input. This runs as a ~400ms synchronous step before the message goes out.
Both Claude Code and Pi support this natively:
- Claude Code:
UserPromptSubmit hook. Can inject additionalContext or block with exit code 2.
- Pi:
input event on the Extension API. Handler receives the text and returns transformed text. Can also fully replace or suppress.
DeepSeek TUI's message_submit event exists but is observe-only. The hook can't change what gets sent. That's the gap.
2. turn_end / agent_idle event (critical)
A hook that fires when the agent finishes responding and goes idle, waiting for the next user input.
Use case: I run a "voice of reason" extension that, when the agent finishes a turn, async-fires a separate model call (with thinking enabled) on the agent's transcript. The critique is stashed and prepended to the next user submission (via the submit hook above). If the agent submits a new turn before the critique arrives, the in-flight critique is dropped. This is a fire-and-forget pattern that never blocks the user.
Prior art:
- Claude Code:
Stop event. Receives a stop_hook_active flag so hooks can detect re-entry and avoid infinite loops.
- Pi:
turn_end event (and separately agent_end for the full agent loop boundary).
3. Subagent lifecycle events (nice to have)
subagent_spawn and subagent_complete events. DeepSeek TUI already has sub-agents as a first-class feature, but they're invisible to hooks. Being able to observe or gate sub-agent creation would close the loop on the orchestration side.
Prior art:
- Claude Code:
SubagentStart / SubagentStop with matcher filtering by agent type.
- Pi: no direct equivalent (different sub-agent model).
Why this matters beyond my use case
The pattern of MCP + hooks is converging across agent harnesses. MCP handles tool portability; hooks handle behavioral portability. Without mutable hooks on submit and turn-end, every extension author who wants to inject context, run background analysis, or wire up cross-session memory has to build a sidecar that replaces the TUI surface. That's a lot of lost UX for something that could be a config stanza.
The existing hook events (session_start, session_end, tool_call_before, tool_call_after, mode_change, on_error, shell_env) are solid for observability. The missing piece is hooks that can participate in the data flow, not just observe it.
Suggested design
For message_submit with mutation: the hook receives the user text on stdin as JSON. If it exits 0 with JSON on stdout containing a text field, that text replaces the original. If it exits 0 with no output or without a text field, the original goes through unchanged. Exit 2 blocks the submission. This matches the pattern Claude Code uses.
For turn_end: the hook receives the turn metadata (model used, token counts, tool calls made) on stdin as JSON. Exit code doesn't block anything since there's nothing to block. Include a stop_hook_active or equivalent flag so hooks can detect if they're being called because of their own injected content.
Both should support the existing [[hooks.hooks]] config format, condition filtering, and default_timeout_secs.
Happy to help spec, test, or PR this.
Problem
I run the same set of behavioral extensions across multiple coding agent harnesses (Claude Code, Pi, and DeepSeek TUI). The portability stack is simple: MCP for tools, hooks for behavioral extensions that inject context or react to agent lifecycle events. DeepSeek TUI has MCP, which is great. But its hook surface doesn't support the two events that matter most for portable extensions, so I had to build an SSE sidecar that spawns
deepseek serve --httpand owns the user-input path entirely, just to get the same extension behavior I get natively in other harnesses.The sidecar works but it replaces the TUI. No slash commands, no status bar, no in-TUI MCP manager, no image rendering. The quality gap compared to running the same extensions as native Pi extensions or Claude Code hooks is noticeable.
What I need
1.
message_submitwith mutation rights (critical)A hook that fires before the user's message is sent to the model, where the hook can modify the message text. This is the single most important missing piece.
Use case: I run a "subconscious memory" extension that, on every user submission, queries a knowledge base for relevant context from past conversations, then prepends it to the user's message inside XML tags. The agent sees the context as part of the input. This runs as a ~400ms synchronous step before the message goes out.
Both Claude Code and Pi support this natively:
UserPromptSubmithook. Can injectadditionalContextor block with exit code 2.inputevent on the Extension API. Handler receives the text and returns transformed text. Can also fully replace or suppress.DeepSeek TUI's
message_submitevent exists but is observe-only. The hook can't change what gets sent. That's the gap.2.
turn_end/agent_idleevent (critical)A hook that fires when the agent finishes responding and goes idle, waiting for the next user input.
Use case: I run a "voice of reason" extension that, when the agent finishes a turn, async-fires a separate model call (with thinking enabled) on the agent's transcript. The critique is stashed and prepended to the next user submission (via the submit hook above). If the agent submits a new turn before the critique arrives, the in-flight critique is dropped. This is a fire-and-forget pattern that never blocks the user.
Prior art:
Stopevent. Receives astop_hook_activeflag so hooks can detect re-entry and avoid infinite loops.turn_endevent (and separatelyagent_endfor the full agent loop boundary).3. Subagent lifecycle events (nice to have)
subagent_spawnandsubagent_completeevents. DeepSeek TUI already has sub-agents as a first-class feature, but they're invisible to hooks. Being able to observe or gate sub-agent creation would close the loop on the orchestration side.Prior art:
SubagentStart/SubagentStopwith matcher filtering by agent type.Why this matters beyond my use case
The pattern of MCP + hooks is converging across agent harnesses. MCP handles tool portability; hooks handle behavioral portability. Without mutable hooks on submit and turn-end, every extension author who wants to inject context, run background analysis, or wire up cross-session memory has to build a sidecar that replaces the TUI surface. That's a lot of lost UX for something that could be a config stanza.
The existing hook events (
session_start,session_end,tool_call_before,tool_call_after,mode_change,on_error,shell_env) are solid for observability. The missing piece is hooks that can participate in the data flow, not just observe it.Suggested design
For
message_submitwith mutation: the hook receives the user text on stdin as JSON. If it exits 0 with JSON on stdout containing atextfield, that text replaces the original. If it exits 0 with no output or without atextfield, the original goes through unchanged. Exit 2 blocks the submission. This matches the pattern Claude Code uses.For
turn_end: the hook receives the turn metadata (model used, token counts, tool calls made) on stdin as JSON. Exit code doesn't block anything since there's nothing to block. Include astop_hook_activeor equivalent flag so hooks can detect if they're being called because of their own injected content.Both should support the existing
[[hooks.hooks]]config format,conditionfiltering, anddefault_timeout_secs.Happy to help spec, test, or PR this.