Skip to main content
hooksSource-backedReview first Safety Privacy

shfmt Shell Format Check Hook for Claude Code

Read-only Claude Code PostToolUse hook that runs shfmt diff checks after Claude writes or edits shell scripts, surfacing POSIX shell, Bash, Zsh, and mksh formatting drift without rewriting files or executing scripts.

by shfmt·added 2026-06-04·
Claude Code
HarnessClaude Code
Trigger:PostToolUse
Review first review before installing

Open the source and read safety notes before installing.

Safety notes

  • This hook runs `shfmt -d` only. It does not pass `-w`, rewrite files, run scripts, install tools, invoke Docker, or execute code from the edited file.
  • The hook exits non-zero when shfmt prints a diff or reports a parse error. Depending on Claude Code settings, this can interrupt the workflow until a human reviews the output.
  • Keep the hook scoped to write/edit tools. Running shfmt after every tool call can add noise and slow down shell-heavy projects.
  • Formatting diffs are style feedback, not a security review. Pair this with ShellCheck, tests, and human review for command safety.
  • shfmt supports POSIX shell, Bash, Zsh, and mksh syntax, but project-specific style choices may still require a team policy.
  • Do not enable automatic rewriting until the team has agreed on shfmt style and reviewed the effect on existing scripts.

Privacy notes

  • shfmt diffs can include file paths, comments, commands, variable names, hostnames, internal paths, and source snippets from edited shell scripts.
  • Hook output can be retained in Claude Code logs, terminal scrollback, screenshots, support tickets, issue comments, or AI transcripts.
  • Avoid pasting proprietary deployment scripts, customer paths, secret variable names, generated tokens, hostnames, or production command output into public comments or prompts.
  • Use synthetic examples when sharing shfmt findings publicly, and review diff output before exposing private repository structure.

Prerequisites

  • Claude Code project where hooks are allowed by user or project policy.
  • shfmt installed locally and available on `PATH`, for example through Go install, Homebrew, a system package, or another official release path.
  • `jq` available on the machine to parse Claude Code hook input.
  • A reviewed `.claude/settings.json` or user settings hook configuration for `Write`, `Edit`, and `MultiEdit`.
  • Agreement that shell formatting diffs should be blocking or interrupting when shfmt reports drift.

Schema details

Install type
cli
Reading time
7 min
Difficulty score
58
Troubleshooting
Yes
Breaking changes
No
Source repository stats
Scope
Source repo
Runtime and command metadata
Trigger
PostToolUse
Script language
bash
Script body
#!/usr/bin/env bash
set -uo pipefail

input="$(cat)"

if ! command -v jq >/dev/null 2>&1; then
  echo "shfmt hook skipped: jq is required to parse Claude Code hook input." >&2
  exit 0
fi

tool_name="$(printf '%s' "$input" | jq -r '.tool_name // .toolName // empty')"
file_path="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.path // .toolInput.file_path // .toolInput.path // empty')"

case "$tool_name" in
  Write|Edit|MultiEdit|write|edit|multiedit) ;;
  *) exit 0 ;;
esac

if [ -z "$file_path" ] || [ ! -f "$file_path" ] || [ ! -r "$file_path" ]; then
  exit 0
fi

case "$file_path" in
  *.sh|*.bash|*.bats|*.ksh|*.zsh) ;;
  *)
    first_line="$(LC_ALL=C sed -n '1p' "$file_path" 2>/dev/null || true)"
    case "$first_line" in
      '#!'*'/sh'*|'#!'*' bash'*|'#!'*'/bash'*|'#!'*' dash'*|'#!'*'/dash'*|'#!'*' ksh'*|'#!'*'/ksh'*|'#!'*' zsh'*|'#!'*'/zsh'*) ;;
      *) exit 0 ;;
    esac
    ;;
esac

if ! command -v shfmt >/dev/null 2>&1; then
  echo "shfmt hook skipped: install shfmt to enable shell formatting checks." >&2
  exit 0
fi

echo "shfmt hook: checking $file_path" >&2
shfmt -d "$file_path"
status=$?

if [ "$status" -ne 0 ]; then
  echo "shfmt hook: formatting drift found. Review the diff or run shfmt intentionally before asking Claude to continue." >&2
fi

exit "$status"
Full copyable content
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "./.claude/hooks/shfmt-shell-format-check.sh"
          }
        ]
      }
    ]
  }
}

About this resource

Overview

This hook runs shfmt after Claude Code writes or edits a shell-like file. It is intentionally read-only: it reports formatting diffs and parse errors, but it does not rewrite files, run scripts, install packages, invoke Docker, or execute commands from the edited file.

Use it when a project contains shell scripts and you want immediate feedback on consistent shell formatting after Claude edits a file. It complements ShellCheck-style diagnostics by focusing on formatting and parser-supported syntax consistency rather than command-safety findings.

Source Review

These sources were reviewed on 2026-06-04. The official mvdan/sh README describes shfmt as the formatter included with the shell parser, formatter, and interpreter project. It documents support for POSIX shell, Bash, Zsh, and mksh, installation through go install mvdan.cc/sh/v3/cmd/shfmt@latest, package availability across common systems, default formatting style, and Docker image availability. This hook intentionally uses a local binary and read-only diff mode.

Installation

  1. Install shfmt locally through Go install, Homebrew, a system package, or another reviewed installation path.
  2. Confirm shfmt --help works in the same environment Claude Code uses.
  3. Confirm jq is available.
  4. Save the script as .claude/hooks/shfmt-shell-format-check.sh.
  5. Make it executable.
  6. Add the hook configuration to .claude/settings.json or user settings after reviewing the behavior with your team.

Hook Configuration

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "./.claude/hooks/shfmt-shell-format-check.sh"
          }
        ]
      }
    ]
  }
}

Hook Script

Save this as .claude/hooks/shfmt-shell-format-check.sh and make it executable:

#!/usr/bin/env bash
set -uo pipefail

input="$(cat)"

if ! command -v jq >/dev/null 2>&1; then
  echo "shfmt hook skipped: jq is required to parse Claude Code hook input." >&2
  exit 0
fi

tool_name="$(printf '%s' "$input" | jq -r '.tool_name // .toolName // empty')"
file_path="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.path // .toolInput.file_path // .toolInput.path // empty')"

case "$tool_name" in
  Write|Edit|MultiEdit|write|edit|multiedit) ;;
  *) exit 0 ;;
esac

if [ -z "$file_path" ] || [ ! -f "$file_path" ] || [ ! -r "$file_path" ]; then
  exit 0
fi

case "$file_path" in
  *.sh|*.bash|*.bats|*.ksh|*.zsh) ;;
  *)
    first_line="$(LC_ALL=C sed -n '1p' "$file_path" 2>/dev/null || true)"
    case "$first_line" in
      '#!'*'/sh'*|'#!'*' bash'*|'#!'*'/bash'*|'#!'*' dash'*|'#!'*'/dash'*|'#!'*' ksh'*|'#!'*'/ksh'*|'#!'*' zsh'*|'#!'*'/zsh'*) ;;
      *) exit 0 ;;
    esac
    ;;
esac

if ! command -v shfmt >/dev/null 2>&1; then
  echo "shfmt hook skipped: install shfmt to enable shell formatting checks." >&2
  exit 0
fi

echo "shfmt hook: checking $file_path" >&2
shfmt -d "$file_path"
status=$?

if [ "$status" -ne 0 ]; then
  echo "shfmt hook: formatting drift found. Review the diff or run shfmt intentionally before asking Claude to continue." >&2
fi

exit "$status"

What It Checks

  • Shell-like file extensions such as .sh, .bash, .bats, .ksh, and .zsh.
  • Executable or extensionless files with common shell shebangs.
  • Formatting drift reported by shfmt -d.
  • Parse errors reported by shfmt for the supported shell dialects.

What It Does Not Do

  • It does not rewrite files.
  • It does not run shell scripts.
  • It does not install shfmt.
  • It does not invoke Docker or use shfmt container images.
  • It does not lint command safety, quoting, or portability beyond parser and formatting behavior.
  • It does not replace ShellCheck, tests, code review, or deployment safeguards.

Troubleshooting

Hook says shfmt is missing

Install shfmt through Go install, Homebrew, a system package, or another reviewed local installation path. Confirm command -v shfmt works in the same shell environment Claude Code uses.

Hook does not run for an extensionless script

The script checks the first line for common shell shebangs. Confirm the file is readable and starts with a shell shebang such as #!/usr/bin/env bash or #!/bin/sh.

shfmt reports a diff but the team prefers the current style

Decide on team formatting policy before making the hook blocking. If the team needs a different shfmt style, adapt the command flags deliberately and document them next to the hook.

shfmt passes but ShellCheck still fails

That is expected. shfmt is a formatter and parser. Keep ShellCheck or another shell static analyzer in the workflow for quoting, expansion, portability, and command-safety diagnostics.

Duplicate Check

No existing content/hooks, content/skills, content/agents, content/mcp, or content/tools entry in this checkout matches shfmt, mvdan/sh, mvdan.cc/sh, or a shell formatting check hook. Open PR titles, branch names, changed files, issue titles, issue bodies, and source URLs were also checked before drafting this entry.

This entry is distinct from the existing ShellCheck hook and Hadolint hook. ShellCheck reports shell diagnostics, Hadolint reports Dockerfile diagnostics, and this hook reports shell formatting diffs through shfmt.

Editorial Disclosure

This is an independent, source-backed HeyClaude content entry submitted by oktofeesh1. It is not sponsored by shfmt, mvdan/sh, or the project maintainers. The hook expects a user-installed shfmt binary and does not package, redistribute, or verify a shfmt release artifact.

#shfmt#shell#bash#claude-code#hooks

Source citations

Signals

Loading live community signals…

More like this, weekly

A short, calm digest of reviewed Claude resources. Unsubscribe any time.