v1 docs hooks go 1.26

[PostToolUse]

scut claude hook post-tool-use — fires after a Claude Code tool call succeeds. Silently formats files written by Claude, applying gofmt for Go source and goldmark-prettier-markdown for Markdown. Formatting is in-place; the hook never blocks tool results or injects context.

2formatters
8dispatch steps
3testing layers
3extensions handled

// 01Overview

Fires after a Claude Code tool call succeeds. The hook silently formats files written by Claude — applying gofmt for Go source and goldmark-prettier-markdown for Markdown. Formatting happens in-place; the hook never blocks tool results or injects context.

// 02Input

Deserialized from stdin as claudecode.PostToolUseInput.

FieldTypeDescription
(base fields) claudecode.Input session_id, transcript_path, cwd, hook_event_name, permission_mode, agent_id, agent_type
tool_name string Name of the tool that was called (e.g., Bash, Edit, mcp__github__search_repositories)
tool_use_id string Unique identifier for this tool invocation
tool_input json.RawMessage The input that was passed to the tool — shape varies per tool
tool_response json.RawMessage The tool's response — shape varies per tool

FilePath extraction

PostToolUseInput.FilePath() extracts tool_input.file_path from the raw JSON. Returns "" if the field is absent, empty, or not a string. This method lives in the shared hooks/claudecode package.

// 03Output

Written to stdout as claudecode.PostToolUseOutput.

FieldTypeDescription
(base fields) claudecode.BaseOutput continue, stopReason, suppressOutput, systemMessage
decision *Decision Set to "block" to reject the tool result
reason *string Explanation sent to Claude when blocking
additionalContext *string Text injected into Claude's context
updatedMCPToolOutput json.RawMessage Replacement output for MCP tools — Claude sees this instead of the original response

The current implementation always returns an empty PostToolUseOutput{}. Formatting is silent — Claude never knows it happened.

// 04Dispatch Flow

  1. Decode PostToolUseInput from stdin
  2. Extract file_path via in.FilePath() — bail if empty
  3. fs.Stat(fp) — bail if file doesn't exist
  4. Switch on filepath.Ext(fp) to select a formatter — bail if no match
  5. afero.ReadFile(fs, fp) — bail on error
  6. Call formatter — bail if result is nil (declined) or unchanged
  7. afero.WriteFile(fs, fp, formatted, info.Mode()) — preserves original file permissions
  8. Write empty PostToolUseOutput{} to stdout
noteEvery bail path writes an empty output and returns nil. The hook never errors on formatting failures — it silently skips.

// 05Byte Formatters

Formatters live in the internal/format package and are pure functions with signature func(src []byte) ([]byte, error):

FunctionExtensionsBackendBehavior on bad input
format.FormatGo .go go/format.Source Returns nil, nil (syntax error — let the compiler catch it)
format.FormatMarkdown .md, .mdx goldmark + goldmark-prettier-markdown with ProseWrapPreserve. Parser extensions enabled for GFM (tables, strikethrough, task checkboxes), footnotes, and definition lists — renderer-only, no HTML renderers added. Returns nil, nil (parse error)

Returning nil, nil means "decline to format" — the command skips the write.

// 06File I/O via afero.Fs

The command receives afero.Fs via Kong dependency injection (see kong-base-setup). In production this is afero.OsFs (real filesystem). In tests it's afero.MemMapFs (in-memory), which allows seeding files and verifying writes without touching disk.

// 07Adding a New Formatter

  1. Add a func Format<Name>(src []byte) ([]byte, error) function to internal/format/format.go (or a sibling file in the same package)
  2. Add test cases under internal/format/ — pure bytes in, bytes out
  3. Add a case to the extension switch in internal/cmd/claude/hook/posttooluse.go that selects format.Format<Name>
  4. Add a dispatch test case in internal/cmd/claude/hook/posttooluse_test.go

// 08Testing

Three layers, each independently testable:

LayerFileWhat it testsMocks
FilePath extraction hooks/claudecode/claudecode_test.go JSON parsing of tool_input.file_path None — pure JSON
Byte formatters internal/format/format_go_test.go, internal/format/format_markdown_test.go Formatting correctness, edge cases None — pure bytes
Command dispatch internal/cmd/claude/hook/posttooluse_test.go Extension routing, file I/O, end-to-end afero.MemMapFs

// 09Code

RolePath
Command internal/cmd/claude/hook/posttooluse.go
Formatters (FormatGo, FormatMarkdown) internal/format/format.go
Types (PostToolUseInput, PostToolUseOutput, FilePath()) hooks/claudecode/claudecode.go

See also: kong-base-setup for how afero.Fs is wired via BindTo, and claude-hook-commands for the full hook command inventory.