Contributing
PostToolUse Formatter
How scut extracts changed paths and dispatches formatters after tool calls.
The PostToolUse formatter is shared in spirit across Claude Code and Codex, but each agent provides a different payload shape.
Claude path extraction
Claude Code PostToolUse events include tool_name, tool_input, and tool_response. For Write and Edit, scut reads tool_input.file_path and formats that file if its extension is supported.
FilePath() returns an empty string when file_path is absent, empty, or non-string. Missing paths are a skip condition, not an error.
Codex path extraction
Codex may provide a direct file_path, a command string containing an apply_patch body, or tool-specific patch text. Scut parses supported patch headers and extracts candidate paths before dispatching formatters.
For Codex, FilePaths() checks direct file_path first, then parses patch text from fields such as command, patch, or input. Relative paths are resolved against cwd. Deleted files are not returned because there is nothing left to format.
Dispatch flow
For each candidate path:
fs.Stat(path); skip missing files.- Discover the formatting root by walking up from the file directory until
.git,.prettierignore, or.scutignoreis found. - Load root
.prettierignore, then root.scutignore. - Skip ignored paths.
- Select a formatter by file extension.
- Read bytes through the injected
afero.Fs. - Run the byte formatter.
- Skip
nilformatter results and unchanged output. - Write formatted bytes back with the original file mode.
- Return an empty hook response unless at least one file changed.
Formatter dispatch
| Extension | Formatter | Behavior |
|---|---|---|
.go | Go formatter | Uses Go parser/formatter APIs and skips syntactically invalid files. |
.md | Markdown formatter | Uses goldmark-prettier-markdown/v2 with Goldmark v2. |
.mdx | Markdown formatter | Uses the same Markdown formatter. |
Ignore files such as .prettierignore and .scutignore can prevent formatting.
For Markdown and MDX files, the shared formatter preserves leading Hugo YAML, TOML, and JSON front matter verbatim. When it cannot safely identify a complete leading front matter block, the formatter returns no result, so the hook leaves the file unchanged.
The Goldmark v2 parser registers tables, strikethrough, task lists, footnotes, and definition lists individually. It intentionally does not use the aggregate GFM parser, which would also enable linkification and broaden the formatter’s syntax policy.
Hooks call FormatMarkdown with scut’s stable defaults: preserved prose wrapping, print width 80, tab width 2, and double-quoted link and image titles. The options exposed by scut format markdown configure only that direct invocation and are not persistent hook configuration.
.scutignore is loaded after .prettierignore, so it can add scut-specific exclusions or re-include paths with ! patterns.
Decision control
The formatter is designed to be transparent. On success it writes formatted files and returns an empty hook response. Unsupported files and missing paths are not errors.
When one or more files are changed, the command returns nested hookSpecificOutput.additionalContext with the event name set to PostToolUse. The message tells the agent that scut reformatted files on disk, so it should not assume its original tool input is byte-for-byte current.
Testing layers
| Layer | Coverage |
|---|---|
| Path extraction | Pure JSON tests for Claude file_path and Codex patch parsing. |
| Byte formatters | Pure byte-in/byte-out tests for Go and Markdown formatting. |
| Ignore matching | Root discovery, pattern precedence, directory globs, and negation. |
| Command dispatch | End-to-end hook tests with afero.MemMapFs, including ignored files and multi-file Codex patches. |