Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 

README.md

go-format

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.

Behavior

  • 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 .go file. 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 makes gofmt itself safe to run unconditionally.
  • Spawned only for Go files. The hook is registered with the if filter Edit(*.go), so a Write/Edit of any other file never starts a hook process for it.
  • Extension-scoped. Only .go files trigger the hook (like ruff-format's *.py/*.pyi filter; unlike typos-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 for addlicense/goheader output. goimports itself has no awareness of that convention, so this hook adds the guard itself.
  • Leaves gitignored files alone. By default a .go file 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. Set go_format_lint_gitignored=true to 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-format autofix pass.
  • Groups local imports using your module's own path. When a go toolchain is on PATH, the hook resolves the edited file's own module path (go list -m) and passes it as goimports' -local grouping 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 -local convention without adding any new consumer config. Falls back to goimports' plain default grouping when go is absent or the file isn't in a resolvable module.
  • Syntax errors surface as advisory findings. When goimports can't parse the file, the parse diagnostic is reported via additionalContext, never auto-"fixed" and never treated as a tool break.
  • Advisory, never blocking. The hook always exits 0. Findings are reported via additionalContext; they never reject the edit. Make a commit hook or CI your hard gate.

Requirements

  • 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 through node 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. Like typos-format, goimports has no per-repo dependency-manager convention. It is conventionally go 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 (a prerequisite notice), renewed with the install route every eighth skip. Install: go install golang.org/x/tools/cmd/goimports@latest (requires a Go toolchain).
  • go on PATH (optional). Used only to resolve the -local grouping prefix (go list -m). Absent: the hook still formats/fixes imports, just without the -local grouping (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.

Hook budget accounting

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.

Install

/plugin marketplace add melodic-software/claude-code-plugins
/plugin install go-format@<marketplace>

Then verify prerequisites with /go-format:setup check.

Configuration

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=false

Options reference

Generated 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.

How to set these

Three supported routes, in the order most people want them:

  1. Interactively. Claude Code prompts for declared options when you enable the plugin. To change them later: /plugin configure go-format@<marketplace>.

  2. Headless. Repeat --config for 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 installed and still writes the value. The short-circuit message is about the install, not the config write. Do not claude plugin uninstall to reconfigure: uninstalling drops this plugin's whole stored pluginConfigs entry, resetting every option in the table above to its default. -s defaults to user, so pass the scope claude plugin list reports 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.

  3. By hand, in settings. Add the value under pluginConfigs in 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's enabledPlugins instead 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.

Upstream documentation

License

MIT (SPDX-License-Identifier: MIT).