PRD: HooksProvider Abstraction — Type-Safe, Replaceable Hooks Layer for Squad
Executive Summary
Squad's hook system is split across two incompatible APIs (HookPipeline in hooks/index.ts and SquadSessionHooks in adapter/types.ts), only 2 of 8 Copilot hooks are wired at runtime, and hook behavior can't be swapped without modifying Squad source. This PRD proposes a HooksProvider interface — following the exact pattern of Squad's StorageProvider — that unifies all 8 hooks behind a single type-safe contract, ships a DefaultHooksProvider that preserves current behavior, and lets users swap in custom implementations via squad.config.ts.
This is a fork-friendly core change — it adds an interface and wiring, doesn't remove anything, and existing HookPipeline code becomes the default implementation behind the new interface.
Problem Statement
Three Problems
1. Two incompatible hook APIs
Squad has two hook surfaces that don't talk to each other:
| API |
Location |
What it does |
What it doesn't do |
HookPipeline |
hooks/index.ts |
Pre/post tool hooks + 5 policies. Fully functional. |
No lifecycle hooks (session start/end, errors, prompts). Not replaceable — it's a concrete class. |
SquadSessionHooks |
adapter/types.ts |
Types for all 6 SDK hooks. Clean interface. |
Never called at runtime. Types are inert. Has no way to register the 2 VS Code-only hooks. |
A user who wants to add an onSessionStart hook has no working path. A user who wants to replace the PII scrubber can't swap it without subclassing HookPipeline.
2. Only 2 of 8 hooks fire at runtime
The Copilot platform supports 8 hooks. Squad's runtime fires exactly 2 (onPreToolUse, onPostToolUse). The other 6 are dead code — typed but never invoked.
3. No provider pattern — hooks aren't swappable
Squad has a proven pattern for swappable I/O: StorageProvider (interface) → FSStorageProvider (default) → users pass custom implementations via config. Hooks don't follow this pattern. HookPipeline is a concrete class constructed directly. You can't replace it without modifying Squad source or forking.
Proposed Solution: HooksProvider Interface
The Pattern (Already Proven in Squad)
StorageProvider (interface) HooksProvider (interface)
├── FSStorageProvider (default) ├── DefaultHooksProvider (default)
├── InMemoryStorageProvider ├── GovernanceHooksProvider
├── SQLiteStorageProvider ├── TelemetryHooksProvider
└── (users swap via config) └── (users swap via config)
Interface Definition
// packages/squad-sdk/src/hooks/hooks-provider.ts
/**
* HooksProvider — abstract lifecycle contract for Squad hooks.
*
* All hook invocations flow through this interface so that
* implementations (default governance, telemetry, enterprise compliance)
* can be swapped without touching business logic.
*
* Follows the same pattern as StorageProvider.
* All methods are optional — return null/undefined to proceed with defaults.
*/
export interface HooksProvider {
// ── Tool Lifecycle (Copilot SDK hooks 1-2, already working) ──────────
/**
* Called before a tool executes.
* Return a decision to allow, deny, or modify the tool call.
* Return null to allow by default.
*/
onPreToolUse?(
input: PreToolUseInput,
context: HookContext
): Promise<PreToolUseOutput | null>;
/**
* Called after a tool executes.
* Can transform results, redact data, or trigger side effects.
* Return null to pass through unchanged.
*/
onPostToolUse?(
input: PostToolUseInput,
context: HookContext
): Promise<PostToolUseOutput | null>;
// ── Session Lifecycle (Copilot SDK hooks 3-6, not yet wired) ─────────
/**
* Called when a session starts.
* Inject context, load preferences, configure the session.
*/
onSessionStart?(
input: SessionStartInput,
context: HookContext
): Promise<SessionStartOutput | null>;
/**
* Called when a user submits a prompt (before LLM processes it).
* Rewrite prompts, capture directives, inject context.
*/
onUserPromptSubmitted?(
input: UserPromptInput,
context: HookContext
): Promise<UserPromptOutput | null>;
/**
* Called when a session ends.
* Cleanup, capture metrics, archive state.
*/
onSessionEnd?(
input: SessionEndInput,
context: HookContext
): Promise<void>;
/**
* Called when an error occurs.
* Implement retry logic, model fallback, alerting.
*/
onErrorOccurred?(
input: ErrorInput,
context: HookContext
): Promise<ErrorRecoveryOutput | null>;
// ── Agent Lifecycle (VS Code hooks 7-8, Squad-specific) ──────────────
/**
* Called before context compaction (pruning).
* Preserve critical state before data is truncated.
*/
onPreCompact?(
input: PreCompactInput,
context: HookContext
): Promise<PreCompactOutput | null>;
/**
* Called when a subagent (child agent) is spawned.
* Validate, track, or modify spawn configuration.
*/
onSubagentStart?(
input: SubagentStartInput,
context: HookContext
): Promise<SubagentStartOutput | null>;
/**
* Called when a subagent finishes.
* Collect results, trigger follow-up work, log completion.
*/
onSubagentStop?(
input: SubagentStopInput,
context: HookContext
): Promise<void>;
}
Hook Input/Output Types
// Shared context for all hooks
export interface HookContext {
sessionId: string;
agentName: string;
teamRoot: string;
timestamp: number;
}
// Pre-tool
export interface PreToolUseInput {
toolName: string;
toolArgs: Record<string, unknown>;
}
export interface PreToolUseOutput {
decision: 'allow' | 'deny' | 'modify';
reason?: string;
modifiedArgs?: Record<string, unknown>;
additionalContext?: string;
}
// Post-tool
export interface PostToolUseInput {
toolName: string;
toolArgs: Record<string, unknown>;
toolResult: unknown;
durationMs: number;
}
export interface PostToolUseOutput {
modifiedResult?: unknown;
additionalContext?: string;
suppressOutput?: boolean;
}
// Session start
export interface SessionStartInput {
model: string;
charterContent?: string;
}
export interface SessionStartOutput {
additionalContext?: string;
modifiedConfig?: Partial<{ model: string }>;
}
// User prompt
export interface UserPromptInput {
prompt: string;
}
export interface UserPromptOutput {
modifiedPrompt?: string;
additionalContext?: string;
capturedDirectives?: string[];
}
// Session end
export interface SessionEndInput {
tokensUsed: number;
toolCallCount: number;
filesModified: string[];
durationMs: number;
}
// Error
export interface ErrorInput {
error: Error;
errorType: 'model' | 'tool' | 'network' | 'permission' | 'context_overflow' | 'unknown';
currentModel?: string;
}
export interface ErrorRecoveryOutput {
retry: boolean;
fallbackModel?: string;
modifiedPrompt?: string;
backoffMs?: number;
}
// Pre-compact
export interface PreCompactInput {
currentTokenCount: number;
maxTokens: number;
compactionStrategy: string;
}
export interface PreCompactOutput {
preserveContext?: string[];
exportState?: Record<string, unknown>;
}
// Subagent
export interface SubagentStartInput {
agentName: string;
model: string;
taskType: string;
charterPath: string;
}
export interface SubagentStartOutput {
decision: 'allow' | 'deny';
reason?: string;
modifiedModel?: string;
additionalContext?: string;
}
export interface SubagentStopInput {
agentName: string;
model: string;
durationMs: number;
tokensUsed: number;
toolCallCount: number;
status: 'completed' | 'error' | 'timeout';
filesModified: string[];
}
DefaultHooksProvider
Wraps the existing HookPipeline — zero behavior change for current users.
// packages/squad-sdk/src/hooks/default-hooks-provider.ts
export class DefaultHooksProvider implements HooksProvider {
private pipeline: HookPipeline;
constructor(config?: PolicyConfig) {
this.pipeline = new HookPipeline(config ?? {});
}
async onPreToolUse(input: PreToolUseInput, ctx: HookContext): Promise<PreToolUseOutput | null> {
const result = await this.pipeline.runPreToolHooks({
toolName: input.toolName,
arguments: input.toolArgs,
agentName: ctx.agentName,
sessionId: ctx.sessionId,
});
return {
decision: result.action === 'block' ? 'deny' : result.action,
reason: result.reason,
modifiedArgs: result.modifiedArguments,
};
}
async onPostToolUse(input: PostToolUseInput, ctx: HookContext): Promise<PostToolUseOutput | null> {
const result = await this.pipeline.runPostToolHooks({
toolName: input.toolName,
arguments: input.toolArgs,
result: input.toolResult,
agentName: ctx.agentName,
sessionId: ctx.sessionId,
});
return { modifiedResult: result.result };
}
// Lifecycle hooks — no-op in default provider (preserves current behavior)
async onSessionStart() { return null; }
async onUserPromptSubmitted() { return null; }
async onSessionEnd() {}
async onErrorOccurred() { return null; }
async onPreCompact() { return null; }
async onSubagentStart() { return null; }
async onSubagentStop() {}
/** Access the underlying HookPipeline for direct hook registration */
getPipeline(): HookPipeline { return this.pipeline; }
getReviewerLockout(): ReviewerLockoutHook { return this.pipeline.getReviewerLockout(); }
}
CompositeHooksProvider
Chains multiple providers — first non-null result wins for decisions, all providers fire for void hooks.
export class CompositeHooksProvider implements HooksProvider {
constructor(private providers: HooksProvider[]) {}
async onPreToolUse(input: PreToolUseInput, ctx: HookContext): Promise<PreToolUseOutput | null> {
for (const p of this.providers) {
const result = await p.onPreToolUse?.(input, ctx);
if (result?.decision === 'deny') return result; // First deny wins
if (result) return result; // First non-null wins
}
return null;
}
async onSessionEnd(input: SessionEndInput, ctx: HookContext): Promise<void> {
// Void hooks — fire all providers (logging, metrics, cleanup all run)
await Promise.all(this.providers.map(p => p.onSessionEnd?.(input, ctx)));
}
// ... same pattern for all hooks
}
Configuration (squad.config.ts)
// User's project — zero changes needed if they don't use hooks
import { defineSquad } from '@bradygaster/squad-sdk';
export default defineSquad({
// Default — same as today
});
// User opts into custom hooks
import { defineSquad } from '@bradygaster/squad-sdk';
import { DefaultHooksProvider, CompositeHooksProvider } from '@bradygaster/squad-sdk/hooks';
import { TelemetryHooksProvider } from './my-telemetry-hooks';
export default defineSquad({
hooks: new CompositeHooksProvider([
new DefaultHooksProvider({
allowedWritePaths: ['src/**'],
scrubPii: true,
}),
new TelemetryHooksProvider({
endpoint: 'https://telemetry.internal/ingest',
}),
]),
});
Wiring Plan: Where Each Hook Fires
| Hook |
Wire Into |
File |
Change Type |
onPreToolUse |
Existing HookPipeline call site |
hooks/index.ts |
Delegate to provider instead of direct pipeline call |
onPostToolUse |
Existing HookPipeline call site |
hooks/index.ts |
Delegate to provider instead of direct pipeline call |
onSessionStart |
After createSession() returns |
agents/lifecycle.ts line ~160 |
New call: await hooksProvider.onSessionStart(...) |
onUserPromptSubmitted |
Before session.sendMessage() |
adapter/client.ts sendMessage method |
New call: await hooksProvider.onUserPromptSubmitted(...) |
onPreCompact |
Before context summarization |
adapter/client.ts or infinite session handler |
New call if compaction event is detectable from SDK |
onSubagentStart |
Inside spawnAgent(), after session creation |
agents/lifecycle.ts line ~200 |
New call: await hooksProvider.onSubagentStart(...) |
onSubagentStop |
In agent destroy/completion handler |
agents/lifecycle.ts destroy method |
New call: await hooksProvider.onSubagentStop(...) |
onSessionEnd |
In session close handler |
adapter/client.ts close/destroy |
New call: await hooksProvider.onSessionEnd(...) |
onErrorOccurred |
In catch blocks across adapter + lifecycle |
Multiple files |
New call: await hooksProvider.onErrorOccurred(...) |
Files Modified (9 files)
| File |
Changes |
hooks/hooks-provider.ts |
NEW — Interface + input/output types |
hooks/default-hooks-provider.ts |
NEW — Wraps existing HookPipeline |
hooks/composite-hooks-provider.ts |
NEW — Chains multiple providers |
hooks/index.ts |
Export new types + provider classes (no breaking changes) |
agents/lifecycle.ts |
Add hooksProvider to LifecycleManagerConfig, wire onSubagentStart/Stop |
adapter/client.ts |
Wire onSessionStart, onSessionEnd, onUserPromptSubmitted, onErrorOccurred |
adapter/types.ts |
No changes needed — SquadSessionHooks still used by SDK passthrough |
runtime/config.ts |
Accept hooks: HooksProvider in squad.config.ts schema |
index.ts (barrel) |
Export HooksProvider and default implementations |
Lines of Code Estimate
| Component |
LOC |
| Interface + types |
~150 |
| DefaultHooksProvider |
~120 |
| CompositeHooksProvider |
~100 |
| Wiring (lifecycle + adapter + config) |
~130 |
| Total new/modified |
~500 |
How This Serves Real Business Needs
Need 1: Enterprise Compliance — "We must audit everything agents do"
Without HooksProvider: Enterprise teams fork Squad, modify HookPipeline directly, maintain a permanent fork. Every Squad upgrade requires rebasing their audit code.
With HooksProvider:
export default defineSquad({
hooks: new CompositeHooksProvider([
new DefaultHooksProvider({ scrubPii: true }),
new ComplianceHooksProvider({
auditEndpoint: 'https://audit.corp.com/ingest',
retentionPolicy: '7-years',
// Fires on every hook — full lifecycle audit
}),
]),
});
Ship compliance as a plugin. Upgrade Squad freely — the interface is stable.
Business value: Unblocks enterprise adoption. Regulated industries (fintech, healthcare, government) require audit trails they control. HooksProvider makes this possible without forking.
Need 2: Cost Control — "Agents ran up a $200 bill overnight"
Without HooksProvider: No way to intercept agent spawns or track spend programmatically. The coordinator's prose instructions about "cost-first model selection" are advisory, not enforced.
With HooksProvider:
class BudgetHooksProvider implements HooksProvider {
private spent = 0;
async onSubagentStart(input, ctx) {
if (this.spent > this.budget * 0.9) {
return { decision: 'deny', reason: 'Budget 90% consumed. Pausing spawns.' };
}
// Force cheap model if budget is tight
if (this.spent > this.budget * 0.7) {
return { decision: 'allow', modifiedModel: 'claude-haiku-4.5' };
}
return { decision: 'allow' };
}
async onSubagentStop(input, ctx) {
this.spent += estimateCost(input.model, input.tokensUsed);
}
async onSessionEnd(input, ctx) {
await writeBudgetReport(this.spent, input);
}
}
Business value: Makes autonomous Squad (Ralph loops, CI pipelines, nightly batches) financially safe. Spend is capped programmatically, not by hope.
Need 3: Multi-Tenant SaaS — "Each customer gets different rules"
Without HooksProvider: Can't parameterize governance per tenant. Every customer gets the same policies or you fork.
With HooksProvider:
function createTenantHooks(tenant: Tenant): HooksProvider {
return new CompositeHooksProvider([
new DefaultHooksProvider({
allowedWritePaths: tenant.allowedPaths,
blockedCommands: tenant.blockedCommands,
}),
new TenantMetricsProvider(tenant.metricsEndpoint),
...(tenant.plan === 'enterprise' ? [new ComplianceHooksProvider()] : []),
]);
}
// Per-tenant session
const session = await client.createSession({
hooks: createTenantHooks(currentTenant).asSessionHooks(),
});
Business value: Enables Squad SDK as an embedded component in SaaS products. Each tenant gets isolated, configurable governance. New market segment for Squad.
Need 4: Developer Productivity — "Agents keep forgetting what we decided"
Without HooksProvider: Context injection is prompt-template-driven. Agents sometimes skip reads.
With HooksProvider:
class ContextInjectionProvider implements HooksProvider {
async onSessionStart(input, ctx) {
const decisions = await readDecisions(ctx.teamRoot);
const history = await readHistory(ctx.teamRoot, ctx.agentName, { maxEntries: 20 });
const skills = await matchSkills(ctx.teamRoot, input.charterContent);
return {
additionalContext: [decisions, history, ...skills].join('\n\n'),
};
}
async onUserPromptSubmitted(input, ctx) {
const directives = detectDirectives(input.prompt);
if (directives.length > 0) {
await writeDirectives(ctx.teamRoot, directives);
return { capturedDirectives: directives };
}
return null;
}
}
Business value: Decisions stick permanently. Directives are captured automatically. Agents start every session with full context — no reliance on agents "reading the right files."
Need 5: Resilient Autonomous Pipelines — "CI bot died at 3am"
Without HooksProvider: Error recovery is prose instructions in squad.agent.md. Rate limits crash sessions.
With HooksProvider:
class ResilienceProvider implements HooksProvider {
private errorCount = 0;
async onErrorOccurred(input, ctx) {
this.errorCount++;
// Circuit breaker
if (this.errorCount > 5) {
await alertOncall('Squad circuit breaker tripped', input.error);
return { retry: false };
}
// Model fallback
if (input.errorType === 'model') {
const next = this.fallbackChain.shift();
if (next) return { retry: true, fallbackModel: next };
}
// Rate limit backoff
if (input.errorType === 'network') {
return { retry: true, backoffMs: Math.pow(2, this.errorCount) * 1000 };
}
return null;
}
}
Business value: Autonomous Squad survives infrastructure failures. CI bots, Ralph loops, and nightly batches recover automatically. PagerDuty fires when they can't.
Need 6: Agent Fleet Observability — "What are 20 Squad instances doing?"
Without HooksProvider: Each Squad instance has its own .squad/ logs. No aggregation.
With HooksProvider:
class ObservabilityProvider implements HooksProvider {
async onSubagentStart(input, ctx) {
await this.metrics.emit('squad.agent.spawn', {
repo: process.env.REPO, agent: input.agentName, model: input.model,
});
return { decision: 'allow' };
}
async onSubagentStop(input, ctx) {
await this.metrics.emit('squad.agent.complete', {
repo: process.env.REPO, agent: input.agentName,
duration: input.durationMs, tokens: input.tokensUsed,
status: input.status,
});
}
async onPostToolUse(input, ctx) {
await this.metrics.emit('squad.tool.call', {
tool: input.toolName, agent: ctx.agentName, duration: input.durationMs,
});
return null;
}
}
Business value: Platform teams get a single Grafana/Datadog dashboard for all Squad instances. Error rates, token spend, agent performance — all visible in one place.
Migration Path
For existing users (zero changes required)
// Before — works exactly the same
export default defineSquad({
hooks: defineHooks({
allowedWritePaths: ['src/**'],
scrubPii: true,
}),
});
// After — defineHooks() internally creates DefaultHooksProvider
// User sees no difference
For users who want custom hooks
// Step 1: Import the provider
import { DefaultHooksProvider, CompositeHooksProvider } from '@bradygaster/squad-sdk/hooks';
// Step 2: Create custom provider implementing only the hooks you need
class MyHooks implements HooksProvider {
async onSessionStart(input, ctx) { /* ... */ }
// All other hooks: undefined = default behavior
}
// Step 3: Compose with defaults
export default defineSquad({
hooks: new CompositeHooksProvider([
new DefaultHooksProvider({ scrubPii: true }),
new MyHooks(),
]),
});
For HookPipeline users (programmatic)
// Before
const pipeline = new HookPipeline({ scrubPii: true });
pipeline.addPreToolHook(myCustomHook);
// After — still works, HookPipeline is not removed
const pipeline = new HookPipeline({ scrubPii: true });
pipeline.addPreToolHook(myCustomHook);
// OR — use the provider pattern for lifecycle hooks
const provider = new DefaultHooksProvider({ scrubPii: true });
provider.getPipeline().addPreToolHook(myCustomHook);
Implementation Priority
| Phase |
What |
Effort |
Breaks anything? |
| Phase 1 |
Interface + types + DefaultHooksProvider wrapping HookPipeline |
2 days |
No — additive only |
| Phase 2 |
Wire onSubagentStart/onSubagentStop into lifecycle.ts |
1 day |
No — no-op in default provider |
| Phase 3 |
Wire onSessionStart/onSessionEnd into adapter |
1 day |
No — no-op in default |
| Phase 4 |
Wire onErrorOccurred with retry/fallback support |
2 days |
No — no-op in default |
| Phase 5 |
Wire onUserPromptSubmitted into adapter |
1 day |
No — no-op in default |
| Phase 6 |
Wire onPreCompact (depends on SDK exposing compaction event) |
1 day |
No — may be deferred if SDK doesn't expose the event |
| Phase 7 |
CompositeHooksProvider + config integration |
1 day |
No |
Total: ~9 days of focused work. Zero breaking changes at any phase.
Success Metrics
- Zero regressions — existing tests pass at every phase
- HooksProvider adopted by 3+ community projects in first quarter
- Custom providers shipped by at least 1 enterprise customer
- Default provider performance — less than 1ms overhead per hook invocation
- All 8 hooks firing — verifiable via telemetry provider
Relationship to PRDs #151 and #152
| PRD |
Approach |
Relationship to this PRD |
| #151 (Core hooks) |
Embed hooks directly into Squad core |
This PRD supersedes #151 — same hooks, but behind a swappable interface |
| #152 (Plugin) |
Ship hooks as an installable plugin |
This PRD enables #152 — the plugin becomes a HooksProvider implementation that users install and configure |
| #153 (This PRD) |
Type-safe, replaceable interface |
Foundation layer — both #151 and #152 benefit from this abstraction |
Ship order: This PRD (#153) first → Plugin PRD (#152) becomes a provider implementation → Core hooks (#151) are already wired by the interface.
This PRD follows the StorageProvider pattern proven in Squad since Wave 1. The interface is the same shape, the config integration is the same, and the migration path is the same: zero changes for existing users, full power for those who opt in.
PRD: HooksProvider Abstraction — Type-Safe, Replaceable Hooks Layer for Squad
Executive Summary
Squad's hook system is split across two incompatible APIs (
HookPipelineinhooks/index.tsandSquadSessionHooksinadapter/types.ts), only 2 of 8 Copilot hooks are wired at runtime, and hook behavior can't be swapped without modifying Squad source. This PRD proposes aHooksProviderinterface — following the exact pattern of Squad'sStorageProvider— that unifies all 8 hooks behind a single type-safe contract, ships aDefaultHooksProviderthat preserves current behavior, and lets users swap in custom implementations viasquad.config.ts.This is a fork-friendly core change — it adds an interface and wiring, doesn't remove anything, and existing
HookPipelinecode becomes the default implementation behind the new interface.Problem Statement
Three Problems
1. Two incompatible hook APIs
Squad has two hook surfaces that don't talk to each other:
HookPipelinehooks/index.tsSquadSessionHooksadapter/types.tsA user who wants to add an
onSessionStarthook has no working path. A user who wants to replace the PII scrubber can't swap it without subclassingHookPipeline.2. Only 2 of 8 hooks fire at runtime
The Copilot platform supports 8 hooks. Squad's runtime fires exactly 2 (
onPreToolUse,onPostToolUse). The other 6 are dead code — typed but never invoked.3. No provider pattern — hooks aren't swappable
Squad has a proven pattern for swappable I/O:
StorageProvider(interface) →FSStorageProvider(default) → users pass custom implementations via config. Hooks don't follow this pattern.HookPipelineis a concrete class constructed directly. You can't replace it without modifying Squad source or forking.Proposed Solution:
HooksProviderInterfaceThe Pattern (Already Proven in Squad)
Interface Definition
Hook Input/Output Types
DefaultHooksProvider
Wraps the existing
HookPipeline— zero behavior change for current users.CompositeHooksProvider
Chains multiple providers — first non-null result wins for decisions, all providers fire for void hooks.
Configuration (squad.config.ts)
Wiring Plan: Where Each Hook Fires
onPreToolUseHookPipelinecall sitehooks/index.tsonPostToolUseHookPipelinecall sitehooks/index.tsonSessionStartcreateSession()returnsagents/lifecycle.tsline ~160await hooksProvider.onSessionStart(...)onUserPromptSubmittedsession.sendMessage()adapter/client.tssendMessage methodawait hooksProvider.onUserPromptSubmitted(...)onPreCompactadapter/client.tsor infinite session handleronSubagentStartspawnAgent(), after session creationagents/lifecycle.tsline ~200await hooksProvider.onSubagentStart(...)onSubagentStopagents/lifecycle.tsdestroy methodawait hooksProvider.onSubagentStop(...)onSessionEndadapter/client.tsclose/destroyawait hooksProvider.onSessionEnd(...)onErrorOccurredawait hooksProvider.onErrorOccurred(...)Files Modified (9 files)
hooks/hooks-provider.tshooks/default-hooks-provider.tshooks/composite-hooks-provider.tshooks/index.tsagents/lifecycle.tsadapter/client.tsadapter/types.tsruntime/config.tshooks: HooksProviderin squad.config.ts schemaindex.ts(barrel)Lines of Code Estimate
How This Serves Real Business Needs
Need 1: Enterprise Compliance — "We must audit everything agents do"
Without HooksProvider: Enterprise teams fork Squad, modify HookPipeline directly, maintain a permanent fork. Every Squad upgrade requires rebasing their audit code.
With HooksProvider:
Ship compliance as a plugin. Upgrade Squad freely — the interface is stable.
Business value: Unblocks enterprise adoption. Regulated industries (fintech, healthcare, government) require audit trails they control. HooksProvider makes this possible without forking.
Need 2: Cost Control — "Agents ran up a $200 bill overnight"
Without HooksProvider: No way to intercept agent spawns or track spend programmatically. The coordinator's prose instructions about "cost-first model selection" are advisory, not enforced.
With HooksProvider:
Business value: Makes autonomous Squad (Ralph loops, CI pipelines, nightly batches) financially safe. Spend is capped programmatically, not by hope.
Need 3: Multi-Tenant SaaS — "Each customer gets different rules"
Without HooksProvider: Can't parameterize governance per tenant. Every customer gets the same policies or you fork.
With HooksProvider:
Business value: Enables Squad SDK as an embedded component in SaaS products. Each tenant gets isolated, configurable governance. New market segment for Squad.
Need 4: Developer Productivity — "Agents keep forgetting what we decided"
Without HooksProvider: Context injection is prompt-template-driven. Agents sometimes skip reads.
With HooksProvider:
Business value: Decisions stick permanently. Directives are captured automatically. Agents start every session with full context — no reliance on agents "reading the right files."
Need 5: Resilient Autonomous Pipelines — "CI bot died at 3am"
Without HooksProvider: Error recovery is prose instructions in
squad.agent.md. Rate limits crash sessions.With HooksProvider:
Business value: Autonomous Squad survives infrastructure failures. CI bots, Ralph loops, and nightly batches recover automatically. PagerDuty fires when they can't.
Need 6: Agent Fleet Observability — "What are 20 Squad instances doing?"
Without HooksProvider: Each Squad instance has its own
.squad/logs. No aggregation.With HooksProvider:
Business value: Platform teams get a single Grafana/Datadog dashboard for all Squad instances. Error rates, token spend, agent performance — all visible in one place.
Migration Path
For existing users (zero changes required)
For users who want custom hooks
For HookPipeline users (programmatic)
Implementation Priority
onSubagentStart/onSubagentStopinto lifecycle.tsonSessionStart/onSessionEndinto adapteronErrorOccurredwith retry/fallback supportonUserPromptSubmittedinto adapteronPreCompact(depends on SDK exposing compaction event)Total: ~9 days of focused work. Zero breaking changes at any phase.
Success Metrics
Relationship to PRDs #151 and #152
HooksProviderimplementation that users install and configureShip order: This PRD (#153) first → Plugin PRD (#152) becomes a provider implementation → Core hooks (#151) are already wired by the interface.
This PRD follows the
StorageProviderpattern proven in Squad since Wave 1. The interface is the same shape, the config integration is the same, and the migration path is the same: zero changes for existing users, full power for those who opt in.