Skip to content

feat: Plugin hook extensions for deterministic behavior at well-defined points #398

@sentry-junior

Description

@sentry-junior

Problem

Plugins today declare credentials, config, runtime dependencies, skills, and MCP surfaces — but they have no way to deterministically enforce behavioral rules at specific operational points. For example, the GitHub plugin needs to force Co-authored-by trailers in every commit, but this currently lives as prompt text in github-code/SKILL.md, which is advisory, not enforceable.

This limitation will recur for other concerns: PR description templates, issue label defaults, commit message validation, response formatting, etc. Junior core should remain naive about what any plugin does with these hooks.

Proposal: Plugin-declared hook contributions

Add an extensions section to plugin.yaml where plugins contribute to core-owned hook points using declarative effects and optional prompt-injected policy files.

Design principles

  1. Core owns hook points and payload schemas — plugins only contribute to known hooks, they don't invent new runtime payloads.
  2. Junior stays naive — it knows how to "ensure a trailer in text." It does not know why GitHub needs one.
  3. Two layers of influence:
    • effects — deterministic, structured transforms (force a trailer, ensure a section, append to a list). These are guaranteed.
    • policies — prompt-injected markdown files attached to hook points. These are advisory behavioral guidance.
  4. Explicit declaration — all hook contributions live in plugin.yaml, not hidden in skill prose.
  5. Deterministic ordering — priority asc → plugin name asc → hook id asc. No filesystem or YAML parse order dependency.
  6. VersionedhookApiVersion: 1 for future schema evolution.

Manifest shape

extensions:
  hookApiVersion: 1
  hooks:
    - id: github.ensure-bot-coauthor
      hook: commit.message.finalize
      priority: 100
      when:
        envPresent: [GITHUB_APP_BOT_NAME, GITHUB_APP_BOT_EMAIL]
      effects:
        - type: text.ensureTrailer
          key: Co-authored-by
          value: "${env.GITHUB_APP_BOT_NAME} <${env.GITHUB_APP_BOT_EMAIL}>"
          dedupe: true
          required: true
          missing: error
      policies:
        - file: hooks/commit-message.md
          inject: prompt

Core-owned hook points (initial set)

Hook point Payload Purpose
commit.message.prepare text + metadata Before commit message is composed
commit.message.finalize text + metadata After commit message is composed, before execution
pull_request.description.prepare text + metadata Before PR body is composed
pull_request.description.finalize text + metadata After PR body is composed, before creation
issue.labels.suggest structured labels When labels are being determined
response.finalize text Before final assistant response

Hook points and payload schemas are versioned and owned by core. Plugins cannot define new hook points.

Declarative effect primitives (initial set)

Effect type Purpose
text.ensureTrailer Ensure a Key: Value trailer exists in text (git trailer format)
text.ensureSection Ensure a markdown section heading exists
text.ensurePrefix Ensure text starts with a value
text.ensureSuffix Ensure text ends with a value
list.appendUnique Append values to a list if not present

Effects are idempotent by design. Running the same effect twice produces the same output.

Conditional execution

when:
  envPresent: [GITHUB_APP_BOT_NAME]    # env vars must be set
  configPresent: [github.repo]          # config keys must be set

Deliberately limited — no arbitrary expressions in v1.

Install-level overrides

Consuming apps can disable specific hook contributions via PluginConfig:

plugins:
  manifests:
    github:
      extensions:
        disabledHooks:
          - github.ensure-bot-coauthor

How other use cases map

PR description templates:

- id: github.pr.template
  hook: pull_request.description.prepare
  effects:
    - type: text.ensureSection
      heading: Summary
    - type: text.ensureSection
      heading: Test Plan

Issue label defaults:

- id: github.issue.default-labels
  hook: issue.labels.finalize
  effects:
    - type: list.appendUnique
      path: labels
      values: [needs-triage]

Response formatting guidance (advisory only):

- id: sentry.response-format
  hook: response.finalize
  policies:
    - file: hooks/response-style.md
      inject: prompt

Implementation considerations

Commit execution path (blocking question)

For commit.message.finalize to be truly deterministic, Junior needs a structured point where the commit message exists as text before git commit executes. Options:

  1. Canonical commit tool/wrapper — Junior commits through a structured operation that runs hooks before shell execution. Cleanest, most enforceable.
  2. Command interception — sandbox intercepts git commit and runs hooks on the message. More invasive.
  3. Prompt-only with validation — hooks inject policy into prompts and a post-hoc validator checks compliance. Weaker but simpler to start.

Recommend starting with option 1: a commit abstraction that skill workflows use, which runs the hook pipeline on the message before executing git commit.

Manifest parser changes

  • Add zod schema for extensions, hooks[], effect types, policy file refs
  • Validate hook ids match ^[a-z][a-z0-9.-]*$
  • Validate hook names against known hook point enum
  • Validate policy file paths exist relative to plugin dir
  • Validate env/config references in effect values

Registry changes

  • Extend PluginDefinition to include resolved hook contributions
  • New getHookContributions(hookPoint: string) export from registry
  • Deterministic sort at registration time

Prompt integration

  • Policy files loaded at skill-load time alongside the plugin runtime boundary preamble
  • Injected as a new <hook-policies> section in the skill prompt

Out of scope for v1

  • Plugin-defined hook points (core-only)
  • Arbitrary expression language in when
  • Code/function hooks (keeps the model fully declarative)
  • Cross-plugin hook dependencies
  • Async/remote hook execution

References

  • Current plugin spec: specs/plugin-spec.md
  • Plugin types: packages/junior/src/chat/plugins/types.ts
  • Plugin registry: packages/junior/src/chat/plugins/registry.ts
  • Skill capabilities spec: specs/skill-capabilities-spec.md
  • GitHub plugin: packages/junior-github/plugin.yaml
  • Commit conventions: packages/junior-github/skills/github-code/SKILL.md

Action taken on behalf of David Cramer.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions