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
- Core owns hook points and payload schemas — plugins only contribute to known hooks, they don't invent new runtime payloads.
- Junior stays naive — it knows how to "ensure a trailer in text." It does not know why GitHub needs one.
- 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.
- Explicit declaration — all hook contributions live in
plugin.yaml, not hidden in skill prose.
- Deterministic ordering — priority asc → plugin name asc → hook id asc. No filesystem or YAML parse order dependency.
- Versioned —
hookApiVersion: 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:
- Canonical commit tool/wrapper — Junior commits through a structured operation that runs hooks before shell execution. Cleanest, most enforceable.
- Command interception — sandbox intercepts
git commit and runs hooks on the message. More invasive.
- 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.
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-bytrailers in every commit, but this currently lives as prompt text ingithub-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
extensionssection toplugin.yamlwhere plugins contribute to core-owned hook points using declarative effects and optional prompt-injected policy files.Design principles
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.plugin.yaml, not hidden in skill prose.hookApiVersion: 1for future schema evolution.Manifest shape
Core-owned hook points (initial set)
commit.message.preparecommit.message.finalizepull_request.description.preparepull_request.description.finalizeissue.labels.suggestresponse.finalizeHook points and payload schemas are versioned and owned by core. Plugins cannot define new hook points.
Declarative effect primitives (initial set)
text.ensureTrailerKey: Valuetrailer exists in text (git trailer format)text.ensureSectiontext.ensurePrefixtext.ensureSuffixlist.appendUniqueEffects are idempotent by design. Running the same effect twice produces the same output.
Conditional execution
Deliberately limited — no arbitrary expressions in v1.
Install-level overrides
Consuming apps can disable specific hook contributions via
PluginConfig:How other use cases map
PR description templates:
Issue label defaults:
Response formatting guidance (advisory only):
Implementation considerations
Commit execution path (blocking question)
For
commit.message.finalizeto be truly deterministic, Junior needs a structured point where the commit message exists as text beforegit commitexecutes. Options:git commitand runs hooks on the message. More invasive.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
extensions,hooks[], effect types, policy file refs^[a-z][a-z0-9.-]*$Registry changes
PluginDefinitionto include resolved hook contributionsgetHookContributions(hookPoint: string)export from registryPrompt integration
<hook-policies>section in the skill promptOut of scope for v1
whenReferences
specs/plugin-spec.mdpackages/junior/src/chat/plugins/types.tspackages/junior/src/chat/plugins/registry.tsspecs/skill-capabilities-spec.mdpackages/junior-github/plugin.yamlpackages/junior-github/skills/github-code/SKILL.mdAction taken on behalf of David Cramer.