anthropics / anthropics/claude-plugins-official
docs(hook-development): systemMessage documented as reaching Claude; additionalContext missing entirely
- Dominant language
- Python
- Stars
- 36.3k
- Forks
- 4.1k
- Avg merge
- 2d 14h
- Merged PRs (30d)
- 539
Description
`plugins/plugin-dev/skills/hook-development/SKILL.md` teaches `systemMessage` as the way a hook talks to Claude, never mentions `hookSpecificOutput.additionalContext` in its 712 lines, and its own runnable examples demonstrate the resulting bug. The failure is silent.
Filing as an issue rather than a PR: I opened #4590 with this fix and it was auto-closed by `close-external-prs.yml`. Following the precedent of #4328, which reports a doc bug in this same file the same way. Ready-to-apply diff at the bottom.
## The inaccurate lines
| Line | Current text |
|------|--------------|
| 151 | `"systemMessage": "Explanation for Claude"` |
| 179 | `- systemMessage included in context` |
| 207 | `"systemMessage": "Additional context"` (Stop) |
| 286 | `"systemMessage": "Message for Claude"` |
| 292 | ``- `systemMessage`: Message shown to Claude`` |
`additionalContext` appears **0 times** in `hook-development/` — neither `SKILL.md` nor `references/advanced.md`.
## What the docs say
From https://docs.claude.com/en/docs/claude-code/hooks:
- `systemMessage` — "Warning message shown to the user", and "A `systemMessage` field is shown to you, not to Claude."
- `hookSpecificOutput` — "It requires a `hookEventName` field set to the event name."
- `permissionDecisionReason` — "For `allow` and `ask`, shown to the user but not Claude. For `deny`, shown to Claude. For `defer`, ignored." Listed for `PreToolUse` only.
- `stopReason` — "Message shown to the user when `continue` is `false`. Not shown to Claude." Distinct from `reason`.
- `reason` with `decision: "block"` — "Required when `decision` is `\"block\"`. Tells Claude why it should continue."
- `additionalContext` is supported on `SessionStart`, `Setup`, `SubagentStart`, `UserPromptSubmit`, `UserPromptExpansion`, `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `PostToolBatch`, `Stop`, `SubagentStop`.
## The skill's own examples demonstrate the bug
`examples/validate-bash.sh:26,32,38` and `examples/validate-write.sh:21,27,33` all emit this shape:
```json
{"hookSpecificOutput": {"permissionDecision": "deny"}, "systemMessage": "Dangerous command detected: rm -rf"}
```
`permissionDecision: "deny"` with no `permissionDecisionReason`, explanation in `systemMessage` — Claude is told the tool was refused and never told why. These are the canonical examples the skill points readers at, so the wrong pattern is taught by demonstration as well as by prose.
## Why it matters
A hook written from this skill renders a message on screen and changes nothing about the agent's behavior, with no error to notice. The same root cause is already reported downstream against `hookify`, whose `warn` path emits `systemMessage` alone and whose `block` path emits `permissionDecision: "deny"` with no `permissionDecisionReason`: #282, #292, #317, #582, #1738 — open since January. Fixing the skill is upstream of all of them, since it is what teaches the pattern.
The file's content has not been revised since it was created on 2025-11-20 (only a filename-case rename since).
## Proposed change
- Correct the five inaccurate `systemMessage` descriptions.
- Add `hookEventName`, `permissionDecisionReason`, `additionalContext` to the PreToolUse output example; correct the Stop example.
- Add a **Reaching Claude vs. Reaching the User** section distinguishing the five fields that are easy to confuse: `additionalContext`, `permissionDecisionReason`, `systemMessage`, `stopReason`, and `reason`.
- List `stopReason` in **Standard Output (All Hooks)**, where it was missing.
- Fix both example scripts: all six payloads now carry `hookEventName` and `permissionDecisionReason`. JSON parses; `bash -n` passes.
Matches existing file conventions — Title Case heading, padded table separator, unwrapped prose.
## Deliberately not changed
- `SKILL.md:480,494,500` (Security Best Practices) use the legacy top-level `{"decision": "deny", "reason": "..."}` shape for PreToolUse rather than `hookSpecificOutput`. Same in `references/patterns.md:328`, `references/migration.md:112,118,168`, `references/advanced.md:332,372`. I could not confirm from the docs whether that shape is still supported for PreToolUse, so I left it rather than guess — worth a maintainer's call as a follow-up.
- The doc hostname. `docs.claude.com/...` 301-redirects to `code.claude.com/...`, but `docs.claude.com` is used across 7 files in `plugins/`; switching one file would make it the odd one out. Repo-wide change, not a rider here.
Documentation and examples only, +42/-12, three files. No manifest, no hook registration, no runtime behavior.
Branch: https://github.com/Antheurus/claude-plugins-official/tree/docs/hook-development-additional-context
Full diff
```diff
diff --git a/plugins/plugin-dev/skills/hook-development/SKILL.md b/plugins/plugin-dev/skills/hook-development/SKILL.md
index 6317441..31b163f 100644
--- a/plugins/plugin-dev/skills/hook-development/SKILL.md
+++ b/plugins/plugin-dev/skills/hook-development/SKILL.md
@@ -145,13 +145,18 @@ Execute before any tool runs. Use to approve, deny, or modify tool calls.
```json
{
"hookSpecificOutput": {
+ "hookEventName": "PreToolUse",
"permissionDecision": "allow|deny|ask",
+ "permissionDecisionReason": "Why - shown to Claude on deny, shown to the user on allow/ask",
+ "additionalContext": "Extra context added to Claude's context window",
"updatedInput": {"field": "modified_value"}
},
- "systemMessage": "Explanation for Claude"
+ "systemMessage": "Warning shown to the user in the terminal - Claude does not see this"
}
```
+When denying a tool, put the explanation in `permissionDecisionReason`. Claude is told the tool was blocked either way, but only that field tells it why.
+
### PostToolUse
Execute after tool completes. Use to react to results, provide feedback, or log.
@@ -176,7 +181,8 @@ Execute after tool completes. Use to react to results, provide feedback, or log.
**Output behavior:**
- Exit 0: stdout shown in transcript
- Exit 2: stderr fed back to Claude
-- systemMessage included in context
+- `hookSpecificOutput.additionalContext`: added to Claude's context window
+- `systemMessage`: shown to the user only - it does not reach Claude
### Stop
@@ -203,11 +209,13 @@ Execute when main agent considers stopping. Use to validate completeness.
```json
{
"decision": "approve|block",
- "reason": "Explanation",
- "systemMessage": "Additional context"
+ "reason": "Required when decision is block - tells Claude why it should continue",
+ "systemMessage": "Warning shown to the user"
}
```
+`reason` reaches Claude only in the `block` case. For feedback that should not block, Stop also accepts `hookSpecificOutput.additionalContext`.
+
### SubagentStop
Execute when subagent considers stopping. Use to ensure subagent completed its task.
@@ -283,13 +291,35 @@ Execute when Claude sends notifications. Use to react to user notifications.
{
"continue": true,
"suppressOutput": false,
- "systemMessage": "Message for Claude"
+ "systemMessage": "Warning shown to the user in the terminal"
}
```
- `continue`: If false, halt processing (default true)
- `suppressOutput`: Hide output from transcript (default false)
-- `systemMessage`: Message shown to Claude
+- `stopReason`: Message shown to the user when `continue` is false. Not shown to Claude
+- `systemMessage`: Warning shown to the **user**. It is not added to Claude's context — use `hookSpecificOutput.additionalContext` to send text to Claude
+
+### Reaching Claude vs. Reaching the User
+
+These go to different places. Mixing them up produces a hook that looks like it works — a message appears on screen — while Claude never receives it and never acts on it. A hook whose purpose is to steer Claude's next action must use `additionalContext`, or `permissionDecisionReason` when denying a tool.
+
+| Field | Delivered to | Available on |
+|-------|--------------|--------------|
+| `hookSpecificOutput.additionalContext` | Claude's context window | `SessionStart`, `Setup`, `SubagentStart`, `UserPromptSubmit`, `UserPromptExpansion`, `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `PostToolBatch`, `Stop`, `SubagentStop` |
+| `hookSpecificOutput.permissionDecisionReason` | Claude on `deny`; the user on `allow` and `ask` | `PreToolUse` |
+| `reason` (with `decision: "block"`) | Claude — why it should continue | `Stop`, `SubagentStop` |
+| `systemMessage` | The user, in the terminal | All hooks |
+| `stopReason` | The user when `continue` is false — **not** Claude | All hooks |
+
+```json
+{
+ "hookSpecificOutput": {
+ "hookEventName": "PostToolUse",
+ "additionalContext": "This file is generated. Edit src/schema.ts and run `bun generate` instead."
+ }
+}
+```
### Exit Codes
diff --git a/plugins/plugin-dev/skills/hook-development/examples/validate-bash.sh b/plugins/plugin-dev/skills/hook-development/examples/validate-bash.sh
index e364324..e35e7ca 100755
--- a/plugins/plugin-dev/skills/hook-development/examples/validate-bash.sh
+++ b/plugins/plugin-dev/skills/hook-development/examples/validate-bash.sh
@@ -23,19 +23,19 @@ fi
# Check for destructive operations
if [[ "$command" == *"rm -rf"* ]] || [[ "$command" == *"rm -fr"* ]]; then
- echo '{"hookSpecificOutput": {"permissionDecision": "deny"}, "systemMessage": "Dangerous command detected: rm -rf"}' >&2
+ echo '{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Dangerous command detected: rm -rf"}}' >&2
exit 2
fi
# Check for other dangerous commands
if [[ "$command" == *"dd if="* ]] || [[ "$command" == *"mkfs"* ]] || [[ "$command" == *"> /dev/"* ]]; then
- echo '{"hookSpecificOutput": {"permissionDecision": "deny"}, "systemMessage": "Dangerous system operation detected"}' >&2
+ echo '{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Dangerous system operation detected"}}' >&2
exit 2
fi
# Check for privilege escalation
if [[ "$command" == sudo* ]] || [[ "$command" == su* ]]; then
- echo '{"hookSpecificOutput": {"permissionDecision": "ask"}, "systemMessage": "Command requires elevated privileges"}' >&2
+ echo '{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "ask", "permissionDecisionReason": "Command requires elevated privileges"}}' >&2
exit 2
fi
diff --git a/plugins/plugin-dev/skills/hook-development/examples/validate-write.sh b/plugins/plugin-dev/skills/hook-development/examples/validate-write.sh
index e665193..4319ec8 100755
--- a/plugins/plugin-dev/skills/hook-development/examples/validate-write.sh
+++ b/plugins/plugin-dev/skills/hook-development/examples/validate-write.sh
@@ -18,19 +18,19 @@ fi
# Check for path traversal
if [[ "$file_path" == *".."* ]]; then
- echo '{"hookSpecificOutput": {"permissionDecision": "deny"}, "systemMessage": "Path traversal detected in: '"$file_path"'"}' >&2
+ echo '{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Path traversal detected in: '"$file_path"'"}}' >&2
exit 2
fi
# Check for system directories
if [[ "$file_path" == /etc/* ]] || [[ "$file_path" == /sys/* ]] || [[ "$file_path" == /usr/* ]]; then
- echo '{"hookSpecificOutput": {"permissionDecision": "deny"}, "systemMessage": "Cannot write to system directory: '"$file_path"'"}' >&2
+ echo '{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Cannot write to system directory: '"$file_path"'"}}' >&2
exit 2
fi
# Check for sensitive files
if [[ "$file_path" == *.env ]] || [[ "$file_path" == *secret* ]] || [[ "$file_path" == *credentials* ]]; then
- echo '{"hookSpecificOutput": {"permissionDecision": "ask"}, "systemMessage": "Writing to potentially sensitive file: '"$file_path"'"}' >&2
+ echo '{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "ask", "permissionDecisionReason": "Writing to potentially sensitive file: '"$file_path"'"}}' >&2
exit 2
fi
```
Contributor guide
No contributing guide indexed for this repository
Research direction
Read plugins/plugin-dev/skills/hook-development/SKILL.md and the examples/validate-bash.sh and examples/validate-write.sh scripts, then compare their output fields with the linked Claude hooks documentation. Done means the documented field destinations and supported events are accurate, both examples use the intended fields, their JSON parses, and bash -n passes for both scripts.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- shell
- Domain
- documentation
- Issue type
- Documentation
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 45/100