A Claude Code plugin that formats Go files and manages their imports the
moment you edit them. On every Write or Edit of a .go file it runs
goimports's -w,
which adds missing imports, removes unused ones, and applies gofmt-
equivalent formatting. When goimports can't parse the file, the hook
surfaces the syntax error back to Claude as advisory context.
- No consumer-config opt-in gate. Unlike sibling formatter plugins
(
ruff-format,typos-format), this hook needs no repository configuration to run on an edited.gofile.goimports' own docs describe it as "a replacement for your editor's gofmt-on-save hook" and it has no meaningful config-divergence axis when left unconfigured. Running it does not impose a style choice a repo hasn't made, the same reasoning that makesgofmtitself safe to run unconditionally. - Spawned only for Go files. The hook is registered with the
iffilterEdit(*.go), so a Write/Edit of any other file never starts a hook process for it. - Extension-scoped. Only
.gofiles trigger the hook (likeruff-format's*.py/*.pyifilter; unliketypos-format's language-agnostic scope). - Skips generated files. A file whose leading comment/blank-line block
contains Go's canonical
// Code generated ... DO NOT EDIT.marker is left untouched. This includes files where a copyright/license header (a//or/* */block) precedes the marker, common foraddlicense/goheaderoutput.goimportsitself has no awareness of that convention, so this hook adds the guard itself. - Leaves gitignored files alone. By default a
.gofile the repository gitignores is neither rewritten nor reported, since a rewrite of an ignored file has no git checkout to undo it. A tracked file that matches an ignore pattern stays in scope. Setgo_format_lint_gitignored=trueto act on gitignored files too. - Fix in place. Formatting and import changes are applied silently. No
advisory noise on a successful fix, the same posture as a successful
ruff-format/typos-formatautofix pass. - Groups local imports using your module's own path. When a
gotoolchain is onPATH, the hook resolves the edited file's own module path (go list -m) and passes it as goimports'-localgrouping prefix, so your package's own internal imports stay in their own group instead of being collapsed into the third-party group, matching goimports' own-localconvention without adding any new consumer config. Falls back to goimports' plain default grouping whengois absent or the file isn't in a resolvable module. - Syntax errors surface as advisory findings. When
goimportscan't parse the file, the parse diagnostic is reported viaadditionalContext, never auto-"fixed" and never treated as a tool break. - Advisory, never blocking. The hook always exits
0. Findings are reported viaadditionalContext; they never reject the edit. Make a commit hook or CI your hard gate.
- Bash. The hook is a Bash script. On native Windows, install Git for Windows so Claude Code can run it under Git Bash.
- Node.js on
PATH. Every hook row launches throughnode hooks/exec-bash.mjs. Without node the hooks do not start and nothing is enforced. - jq on
PATH. Parses the hook payload. Absent: the hook skips with a visible notice, once per session and agent, renewed every eighth skip. Install jq. - goimports on
PATH. Liketypos-format,goimportshas no per-repo dependency-manager convention. It is conventionallygo installed to the machine-global$GOPATH/bin. It is never downloaded on the fly; if it is not present, the hook skips with a visible notice, once per session (aprerequisitenotice), renewed with the install route every eighth skip. Install:go install golang.org/x/tools/cmd/goimports@latest(requires a Go toolchain). goonPATH(optional). Used only to resolve the-localgrouping prefix (go list -m). Absent: the hook still formats/fixes imports, just without the-localgrouping (goimports' plain default behavior).
A SessionStart probe reports a missing goimports wherever the plugin is enabled and
go_format_enabled is not false, including a repository with no .go files, and names
/go-format:check. It installs nothing.
The hook itself runs on Bash 3.2+. Telemetry timing uses EPOCHREALTIME
(Bash 5.0+); on older bash the telemetry envelope is skipped while
formatting still runs.
Per docs/conventions/hook-budget/README.md,
this hook is always-on for every Write and Edit of a .go file (every handler in
hooks/hooks.json carries the one if row, which keeps every other extension from spawning it,
and the suite pins the whole handler set to the script's own extension set), so its cost on a
clean Go file is the figure that counts. Measured on Linux x86_64 under bash 5.2 in a container
with HOOK_TELEMETRY_SINK and CLAUDE_PROJECT_DIR unset, twelve interleaved trials against an
interleaved bash -c : floor S of about 4 ms, with a kernel census from
strace -f -e trace=clone,clone3,fork,vfork,execve (2026-09-07, 0.3.46):
| Event | Fires | Wall | Spawn-equivalents | Kernel census |
|---|---|---|---|---|
PostToolUse Write, clean .go (no opt-in; goimports always runs) |
1 | 63 ms | 15.0 | 35 to 37 process creations (the Go runtimes' own threads vary), 12 execs: goimports and the go env it runs, the hook's own go list -m module-path probe, two git rev-parse (the working-tree probe and the root resolver), jq, realpath, the disclosure snapshot's mktemp, cp, cmp, rm, and the hook's own bash |
PostToolUse Write, any other extension |
0 | none | 0 | no process; the if row drops the handler before a spawn |
At a 4 ms floor the spawn-equivalent column mostly measures goimports' own run time rather
than spawns, so the census column is the number that transfers to the Windows 11 Git Bash
reference host the convention calls binding; that host's figure for this plugin has not been
taken. The residual is goimports and the go list probe plus the shared library's payload
reader (jq), working-tree probe and root resolver (git twice, realpath) and the
disclosure snapshot.
/plugin marketplace add melodic-software/claude-code-plugins
/plugin install go-format@<marketplace>Then verify prerequisites with /go-format:setup check.
There are no rules to configure. goimports runs with no consumer-config
surface to read. Two userConfig options tune the hook itself:
| Option | Default | Effect |
|---|---|---|
go_format_enabled |
true |
Kill switch. Set false for a clean no-op. |
go_format_lint_gitignored |
false |
Set true to also act on files the repository gitignores. |
Set it interactively with /plugin configure go-format@<marketplace>, or headless on the
install command:
claude plugin install go-format@<marketplace> --config go_format_enabled=falseGenerated from this plugin's .claude-plugin/plugin.json. Every option Claude Code
will prompt for when the plugin is enabled, with the environment variable each hook
reads it from.
| Option | Type | Default | Environment variable | Description |
|---|---|---|---|---|
go_format_enabled |
boolean | true |
CLAUDE_PLUGIN_OPTION_GO_FORMAT_ENABLED |
Run goimports -w on edit of a Go file |
go_format_lint_gitignored |
boolean | false |
CLAUDE_PLUGIN_OPTION_GO_FORMAT_LINT_GITIGNORED |
By default the hook leaves a file the repository gitignores alone: it is not rewritten, since a rewrite of an ignored file has no git checkout to undo it. Set true to act on gitignored files too. A tracked file that matches an ignore pattern is always in scope. |
Three supported routes, in the order most people want them:
-
Interactively. Claude Code prompts for declared options when you enable the plugin. To change them later:
/plugin configure go-format@<marketplace>. -
Headless. Repeat
--configfor each option. Replace<marketplace>with the marketplace you installed this plugin from:claude plugin install go-format@<marketplace> -s <scope> --config go_format_enabled=<value>
The same command reconfigures a plugin that is already installed: it prints
already installedand still writes the value. The short-circuit message is about the install, not the config write. Do notclaude plugin uninstallto reconfigure: uninstalling drops this plugin's whole storedpluginConfigsentry, resetting every option in the table above to its default.-sdefaults touser, so pass the scopeclaude plugin listreports for this plugin. The verified-version record lives in the plugin-reconfiguration convention.The value is stored immediately; the session you are in does not change. Hooks are handed their
CLAUDE_PLUGIN_OPTION_*when the session starts, so start a fresh Claude Code session before expecting new behavior. A check run in the old session still reports the old value, and that is not a failed write. -
By hand, in settings. Add the value under
pluginConfigsin your user settings (~/.claude/settings.json):{ "pluginConfigs": { "go-format@<marketplace>": { "options": { "go_format_enabled": <value> } } } }Plugin option values are read from user,
--settings, and managed settings only, not from a project's.claude/settings.json. To vary behavior per repository, enable or disable the plugin in that project'senabledPluginsinstead of setting an option there.
Do not set the CLAUDE_PLUGIN_OPTION_* variables yourself. They are how Claude Code
hands a configured value to a hook process; the value comes from the routes above.
- User configuration: the
userConfigschema and theCLAUDE_PLUGIN_OPTION_<KEY>export - Plugin install options: the
--configflag's reference entry - Plugins and skills settings:
enabledPlugins,extraKnownMarketplaces,pluginConfigs - Settings files and who they affect: user vs project vs local precedence
- Manage installed plugins: enabling, disabling,
/plugin list
MIT (SPDX-License-Identifier: MIT).