Skip to content

feat(types): add hook contracts and policies - #1153

Open
taltas wants to merge 1 commit into
mainfrom
fm/zoo-hooks-contracts
Open

feat(types): add hook contracts and policies#1153
taltas wants to merge 1 commit into
mainfrom
fm/zoo-hooks-contracts

Conversation

@taltas

@taltas taltas commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Stack

This is 1 of 4 in the Zoo Code hooks MVP stack. Review and merge from the bottom upward. Do not merge a later PR before its predecessor.

Position Pull request Base Scope
1 (this PR) #1153 main Hook contracts and policies
2 #1154 fm/zoo-hooks-contracts Global settings and Hooks panel
3 #1156 fm/zoo-hooks-settings Runner and sessionStart integration
4 #1155 fm/zoo-hooks-session-start preToolUse, E2E, and documentation

Summary

Define the shared, runtime-validated contract for the hooks MVP without executing hooks or exposing settings yet.

Scope

  • Add schemas and TypeScript types for hook definitions, phases, invocations, decisions, and persisted run results.
  • Support sessionStart and preToolUse phases with exact tool-name matching.
  • Centralize fixed output limits, exit-code policy, truncation behavior, and safe summary generation.
  • Add structured say: "hook" chat-message data to the shared message contract.
  • Export the hook API through @roo-code/types.
  • Cover valid definitions, malformed input, matching, output bounds, and exit decisions with pure unit tests.

Data Flow

flowchart LR
    A[Hook definition] --> B[Schema validation]
    B --> C{Phase}
    C -->|sessionStart| D[Lifecycle match]
    C -->|preToolUse| E[Exact tool-name match]
    D --> F[Invocation contract]
    E --> F
    F --> G[Exit and output policy]
    G --> H[Structured hook result]
Loading

Tests

  • packages/types/src/__tests__/hooks.test.ts
  • pnpm --dir src exec eslint --prune-suppressions --max-warnings=0 on all subsequently touched core test files, with no suppression-count increase.
  • Full stack validation at the top branch: pnpm test (437 files passed, 7,356 tests passed, 39 skipped).
  • Full stack validation at the top branch: pnpm check-types, pnpm lint, and pnpm build passed.
  • Full stack VS Code smoke validation: USE_MOCK=true TEST_FILE=hooks.test pnpm --filter @roo-code/vscode-e2e test:run passed.

Risks

  • These exported schemas become the contract consumed by every later layer. The definitions are deliberately narrow so unsupported phases and ambiguous matcher behavior fail validation instead of being inferred.
  • Output and summary limits are fixed policy constants. Later changes to those values can affect persisted records and extension-host memory use.
  • This PR has no process execution or settings UI behavior by itself; those behaviors are isolated in later PRs.

Review Notes

  • The PR is independently buildable against main.
  • No changeset is included, per repository guidance.
  • Merge order is #1153 -> #1154 -> #1156 -> #1155.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Added hook schemas and public types, phase and tool matching, exit classification, UTF-8-safe output handling, sanitization, top-level exports, hook message support, and comprehensive contract tests.

Changes

Hook support

Layer / File(s) Summary
Hook contracts and message integration
packages/types/src/hooks.ts, packages/types/src/message.ts, packages/types/src/index.ts, packages/types/src/__tests__/hooks.test.ts
Defines hook configuration, invocation, result, and message schemas. Adds duplicate-ID validation, immutable defaults, hook message support, package exports, and contract tests.
Hook matching, exit, and output policies
packages/types/src/hooks.ts, packages/types/src/__tests__/hooks.test.ts
Adds phase and tool matching, phase-specific exit classification, UTF-8-safe capture and model-output truncation, byte-count reporting, and ANSI/non-printable character sanitization. Tests cover these policies and edge cases.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Suggested reviewers: edelauna

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains scope, implementation, tests, risks, and review order, but it omits the required linked issue and pre-submission checklist. Add the Related GitHub Issue section with an approved issue number and complete the required pre-submission checklist.
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely identifies the addition of hook contracts and policies.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fm/zoo-hooks-contracts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/types/src/hooks.ts`:
- Around line 261-264: Update sanitizeHookOutput so its non-printable-character
regex excludes the C1 control range U+0080–U+009F while continuing to preserve
valid Unicode characters above it. Add regression coverage verifying that an
8-bit CSI sequence such as \u009b31m is removed from sanitized output.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ca8ee0f6-9e93-412a-8d18-43342c6d2f93

📥 Commits

Reviewing files that changed from the base of the PR and between 64d6e37 and 3c60674.

📒 Files selected for processing (4)
  • packages/types/src/__tests__/hooks.test.ts
  • packages/types/src/hooks.ts
  • packages/types/src/index.ts
  • packages/types/src/message.ts

Comment on lines +261 to +264
export function sanitizeHookOutput(output: string): string {
// ANSI control sequences are removed before other non-printable characters.
// eslint-disable-next-line no-control-regex
return output.replace(/\u001B\[[0-?]*[ -/]*[@-~]/g, "").replace(/[^\t\n\r\x20-\x7E\u0080-\uFFFF]/g, "")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Remove C1 control characters from sanitized output.

The second regex preserves U+0080 through U+009F. These are C1 control characters. For example, sanitizeHookOutput("\u009b31mred") retains an 8-bit CSI sequence. Exclude this range and add regression coverage.

Proposed fix
-	return output.replace(/\u001B\[[0-?]*[ -/]*[`@-`~]/g, "").replace(/[^\t\n\r\x20-\x7E\u0080-\uFFFF]/g, "")
+	return output.replace(/\u001B\[[0-?]*[ -/]*[`@-`~]/g, "").replace(/[^\t\n\r\x20-\x7E\u00A0-\uFFFF]/g, "")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function sanitizeHookOutput(output: string): string {
// ANSI control sequences are removed before other non-printable characters.
// eslint-disable-next-line no-control-regex
return output.replace(/\u001B\[[0-?]*[ -/]*[@-~]/g, "").replace(/[^\t\n\r\x20-\x7E\u0080-\uFFFF]/g, "")
export function sanitizeHookOutput(output: string): string {
// ANSI control sequences are removed before other non-printable characters.
// eslint-disable-next-line no-control-regex
return output.replace(/\u001B\[[0-?]*[ -/]*[`@-`~]/g, "").replace(/[^\t\n\r\x20-\x7E\u00A0-\uFFFF]/g, "")
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/types/src/hooks.ts` around lines 261 - 264, Update
sanitizeHookOutput so its non-printable-character regex excludes the C1 control
range U+0080–U+009F while continuing to preserve valid Unicode characters above
it. Add regression coverage verifying that an 8-bit CSI sequence such as
\u009b31m is removed from sanitized output.

@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-actions github-actions Bot added the awaiting-review PR changes are ready and waiting for maintainer re-review label Aug 5, 2026

@edelauna edelauna left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had some implementation comments

phase: hookPhaseSchema,
status: hookRunStatusSchema,
exitCode: z.number().int().optional(),
stdoutSummary: z.string().optional(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stdoutSummary/stderrSummary here (and outputSummary/errorSummary on hookMessageSchema) are unbounded, unlike every other string in this file. The 64K/16K caps live only in the runtime truncators, so a summary that skips them (tampered history file, IPC, or a future caller) passes validation at multi-MB. Bounding these at the schema (.max(...)) keeps the size invariant on the trust boundary itself. Note .max counts characters, not bytes.


export const DEFAULT_HOOK_DEFINITIONS: readonly HookDefinition[] = Object.freeze([])

export const hookInvocationSchema = z.discriminatedUnion("phase", [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both branches repeat version/hookRunId/taskId/instanceId/workspacePath verbatim; only preToolUse adds tool. hookDefinitionSchema above already uses a base + .extend per phase. Extending a shared base here removes the duplication and stops the two branches drifting when a shared field changes.

return definitions.filter((definition) => definition.enabled && hookMatchesDefinition(definition, phase, toolName))
}

function hookMatchesDefinition(definition: HookDefinition, phase: HookPhase, toolName?: ToolName): boolean {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hookMatchesDefinition just re-expresses the phase branching the hookMatches implementation already does, and re-checks toolName a second time. getMatchingHooks can dispatch inline (phase === "sessionStart" ? hookMatches(d, "sessionStart") : hookMatches(d, "preToolUse", toolName!)), dropping this helper and one guard. Keeps one source of matching logic.

@github-actions github-actions Bot added awaiting-author PR is waiting for the author to address requested changes and removed awaiting-review PR changes are ready and waiting for maintainer re-review labels Aug 18, 2026
@github-actions

github-actions Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Review status

Thanks for contributing. This comment tracks the review sequence and the next action.

Current step: Address maintainer or CODEOWNER feedback, then push an update.

Review-state labels are managed by this workflow; do not edit them manually.

@github-actions github-actions Bot added coderabbit-review-active Required CI passed; CodeRabbit review is active awaiting-coderabbit Waiting for CodeRabbit to approve the latest commit awaiting-author PR is waiting for the author to address requested changes and removed awaiting-author PR is waiting for the author to address requested changes coderabbit-review-active Required CI passed; CodeRabbit review is active awaiting-coderabbit Waiting for CodeRabbit to approve the latest commit labels Aug 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting-author PR is waiting for the author to address requested changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants