Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ user opts in with `/plugin enable`; an existing install is never flipped by cata
- [`bash-format`](plugins/bash-format) — Auto-format and lint shell scripts on edit via shfmt + ShellCheck, using the consuming repo's own .editorconfig and .shellcheckrc.
- [`biome-format`](plugins/biome-format) — Auto-format and lint JS/TS/JSX/JSON on edit via Biome, only when a biome.json governs the repo — using the consuming repo's own Biome config.
- [`ruff-format`](plugins/ruff-format) — Auto-format and lint Python on edit via Ruff, only when a Ruff config governs the repo — using the consuming repo's own Ruff config.
- [`typos-format`](plugins/typos-format) — Auto-fix spelling typos on edit via typos-cli, only when a typos config governs the repo — using the consuming repo's own typos configuration.
- [`typos-format`](plugins/typos-format) — Auto-fix spelling typos on edit via typos-cli, unconditionally — honoring the consuming repo's own typos configuration when one is present.
- [`eol-normalizer`](plugins/eol-normalizer) — Normalize a written file's working-tree line endings to its .gitattributes eol value on edit — symmetric CRLF/LF driven by git check-attr, advisory and never blocking.
- [`powershell-format`](plugins/powershell-format) — Auto-format and lint PowerShell on edit via PSScriptAnalyzer, only when a PSScriptAnalyzerSettings.psd1 governs the repo — using the consuming repo's own analyzer settings.
- [`actionlint`](plugins/actionlint) — Lint GitHub Actions workflow files on edit via actionlint, surfacing findings as advisory context.
Expand Down
6 changes: 3 additions & 3 deletions plugins/typos-format/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "typos-format",
"version": "0.1.0",
"description": "Auto-fix spelling typos on edit via typos-cli, only when a typos config governs the repo — using the consuming repo's own typos configuration.",
"version": "0.2.0",
"description": "Auto-fix spelling typos on edit via typos-cli, unconditionally — honoring the consuming repo's own typos configuration when one is present.",
"author": {
"name": "Melodic Software",
"email": "info@melodicsoftware.com"
Expand All @@ -19,7 +19,7 @@
"typos_format_enabled": {
"type": "boolean",
"title": "typos-format hook",
"description": "Run typos --write-changes on edit of a file, when a typos config governs the repo",
"description": "Run typos --write-changes on edit of any file, unconditionally",
"default": true
}
}
Expand Down
15 changes: 15 additions & 0 deletions plugins/typos-format/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,21 @@
All notable changes to the `typos-format` plugin are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning.

## [0.2.0]

### Changed

- **Removed the opt-in config-gate.** The hook now runs `typos --write-changes`
unconditionally on every `Write`/`Edit`, matching `markdown-format`'s existing
unconditional pattern — typos ships a built-in spelling dictionary and needs
no configuration to be useful. Previously the hook silently no-op'd on any
repo without a hand-authored `typos.toml`/`_typos.toml`/`.typos.toml`/
`Cargo.toml`/`pyproject.toml`, defeating the plugin's zero-config auto-fix
purpose on exactly the repos it was meant to help. A consumer typos config,
when present, is still discovered and honored automatically by typos itself
(allowlist/exclude) — this hook never re-implemented that discovery and
still doesn't; only the activation gate is removed.

## [0.1.0]

### Added
Expand Down
30 changes: 14 additions & 16 deletions plugins/typos-format/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,22 @@ On every `Write` or `Edit` it runs [typos](https://github.com/crate-ci/typos)'s
Claude as advisory context — including remediation guidance for allowlisting
a false positive.

It uses **your repository's own typos configuration**. It ships no rules of
its own and runs only when your repo has opted into typos.
It ships no rules of its own and runs unconditionally, using typos' built-in
spelling dictionary. If your repository has its own typos configuration
(`typos.toml`, `_typos.toml`, `.typos.toml`, `Cargo.toml` with
`[workspace.metadata.typos]`/`[package.metadata.typos]`, or `pyproject.toml`
with `[tool.typos]`), typos discovers and honors it automatically — no
opt-in required.

## Behavior

- **Opt-in on a typos config.** typos runs **only when a `typos.toml`,
`_typos.toml`, `.typos.toml`, `Cargo.toml` (with
`[workspace.metadata.typos]`/`[package.metadata.typos]`), or `pyproject.toml`
with a `[tool.typos]` section governs the edited file**, found by walking up
from the file to the repository root, in that precedence order — the same
discovery typos itself uses. A repo without a typos config is left untouched
rather than checked against typos' built-in dictionary, so the plugin never
imposes a check you did not choose.
- **Runs on every edit, zero-config or not.** typos ships a built-in spelling
dictionary and needs no configuration to be useful, so this hook never gates
on a consumer typos config existing. When a config IS present, typos' own
file-anchored discovery still finds and applies it (allowlist/exclude), in
its documented precedence order — this plugin never re-implements that walk.
- **No extension filter.** Unlike sibling formatter plugins (Ruff, Markdown),
typos is language-agnostic — it runs on any edited file, gated only by the
config opt-in above.
typos is language-agnostic — it runs on any edited file.
- **Fix in place.** `typos --write-changes` applies every correction it has
confidence in. Residual findings — an entry with no known correction (e.g.
a blank-correction `extend-words` entry marking a term "disallowed") —
Expand Down Expand Up @@ -59,10 +59,8 @@ locking/ordering primitive exists in Claude Code today.
per-repo dependency-manager convention — it is a standalone Rust binary,
installed at the machine level (cargo, Homebrew, Conda, pacman, or a
pre-built binary). typos is never downloaded on the fly; if it is not
present while a typos config governs the repo, the hook skips with a
visible once-per-session notice. [Install typos](https://github.com/crate-ci/typos#install).
- A **typos config** (`typos.toml`, `_typos.toml`, `.typos.toml`, or an
equivalent `Cargo.toml`/`pyproject.toml` section) in the repo — the opt-in.
present, the hook skips with a visible once-per-session notice.
[Install typos](https://github.com/crate-ci/typos#install).

The hook itself runs on Bash 3.2+. Telemetry timing uses `EPOCHREALTIME`
(Bash 5.0+); on older bash the telemetry envelope is skipped while typo
Expand Down
71 changes: 19 additions & 52 deletions plugins/typos-format/hooks/typos-format.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@
# entry) findings surface via additionalContext but never block the edit. A
# commit hook or CI is the hard gate.
#
# Opt-in: typos runs ONLY when a typos configuration governs the edited file —
# found by walking up from the file to the repo root, checking (in precedence
# order, per crate-ci/typos' own docs at
# https://github.com/crate-ci/typos/blob/master/docs/reference.md)
# typos.toml, _typos.toml, .typos.toml, Cargo.toml (with
# [workspace.metadata.typos] or [package.metadata.typos]), or pyproject.toml
# (with [tool.typos]). A repo that has not adopted a typos config is left
# untouched rather than checked against typos' built-in defaults, so the
# plugin never imposes a style it did not choose. The typos binary is resolved
# Unconditional: typos ships a built-in spelling dictionary and runs with zero
# configuration, so this hook runs on every edit regardless of whether the
# repo has adopted a typos config — matching the sibling markdown-format
# hook's unconditional pattern. When a config IS present (typos.toml,
# _typos.toml, .typos.toml, Cargo.toml with [workspace.metadata.typos] or
# [package.metadata.typos], or pyproject.toml with [tool.typos], per
# crate-ci/typos' own docs at
# https://github.com/crate-ci/typos/blob/master/docs/reference.md), typos'
# own file-anchored discovery finds and honors it (allowlist/exclude) — this
# hook never re-implements that walk itself. The typos binary is resolved
# from PATH only — never downloaded (typos is a standalone Rust binary with no
# per-repo dependency-manager convention, unlike ruff's .venv).
#
Expand Down Expand Up @@ -82,7 +83,7 @@ FILE=$(printf '%s' "$INPUT" | hook::read_file_path) || exit 0

TOOL=$(printf '%s' "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)

# Resolve repo root early — used to bound the config opt-in walk and to compute
# Resolve repo root early — used as the CWD typos runs in and to compute
# the schema-required repo-relative path in data.file.
REPO_ROOT="$(hook::repo_root "$(dirname "$FILE")")"
# Repo-relative path: schema requires "relative to the consuming repo root".
Expand Down Expand Up @@ -122,52 +123,18 @@ emit_skipped() {
exit 0
}

# Resolve the file's directory in `pwd` form once. Both walks below start here.
FILE_DIR_POSIX="$(cd "$(dirname "$FILE")" 2>/dev/null && pwd)" || FILE_DIR_POSIX=""
root="$(cd "$REPO_ROOT" 2>/dev/null && pwd)" || root=""

# Consumer opt-in: a typos configuration that governs the edited file. Walk up
# from the file's directory to the repo root, stopping at the FIRST config
# found — typos itself resolves the closest config per-directory (checking all
# recognized names together before ascending, per crate-ci/typos'
# Config::from_dir), so the closest hit is exactly the config that will govern
# the run. Same-directory precedence mirrors typos' own documented order:
# typos.toml > _typos.toml > .typos.toml > Cargo.toml (workspace/package
# metadata.typos) > pyproject.toml ([tool.typos]). Absence of any config is the
# opt-out: the file is left untouched.
CONFIG_FOUND=""
dir="$FILE_DIR_POSIX"
while [[ -n "$dir" ]]; do
for name in typos.toml _typos.toml .typos.toml; do
[[ -f "$dir/$name" ]] && CONFIG_FOUND="$dir/$name" && break
done
if [[ -z "$CONFIG_FOUND" && -f "$dir/Cargo.toml" ]] &&
grep -qE '^[[:space:]]*\[(workspace|package)\.metadata\.typos(\]|[.])' "$dir/Cargo.toml" 2>/dev/null; then
CONFIG_FOUND="$dir/Cargo.toml"
fi
if [[ -z "$CONFIG_FOUND" && -f "$dir/pyproject.toml" ]] &&
grep -qE '^[[:space:]]*\[tool\.typos(\]|[.])' "$dir/pyproject.toml" 2>/dev/null; then
CONFIG_FOUND="$dir/pyproject.toml"
fi
[[ -n "$CONFIG_FOUND" ]] && break
[[ -n "$root" && "$dir" == "$root" ]] && break
parent="$(dirname "$dir")"
[[ "$parent" == "$dir" ]] && break # reached filesystem root
dir="$parent"
done

[[ -n "$CONFIG_FOUND" ]] || emit_skipped

# Resolve the typos binary from PATH — never downloaded (typos is a standalone
# Rust binary; no per-repo dependency-manager convention exists for it, unlike
# ruff's .venv or markdownlint's node_modules).
TYPOS_BIN="$(command -v typos 2>/dev/null)" || TYPOS_BIN=""

# The repo opted in via a typos config but no binary is available → visible
# once-per-session skip notice, not a silent gap (dim-9 doctrine).
# No binary available → visible once-per-session skip notice, not a silent gap
# (dim-9 doctrine).
if [[ -z "$TYPOS_BIN" ]]; then
if hook::notice_once "typos-format-typos" "$INPUT"; then
hook::emit_skip_notice PostToolUse "typos-format: a typos config governs this repo but no 'typos' binary was found on PATH — spell-check skipped for this session. Install: https://github.com/crate-ci/typos#install"
hook::emit_skip_notice PostToolUse "typos-format: no 'typos' binary was found on PATH — spell-check skipped for this session. Install: https://github.com/crate-ci/typos#install"
fi
emit_skipped
fi
Expand All @@ -178,11 +145,11 @@ fi
# line, not the process CWD (verified empirically: running from the repo root
# with a relative subdirectory path still discovers and honors that
# subdirectory's own config) — so running from repo root here does not change
# which config governs; it matches the walk above regardless of nesting depth.
# Falls back to the absolute path when the repo root did not resolve or the
# file is outside it.
# which config governs, regardless of nesting depth. Falls back to the
# absolute path and the file's own directory when the repo root did not
# resolve or the file is outside it.
TYPOS_ARG="$FILE"
RUN_DIR="${root:-$FILE_DIR_POSIX}"
RUN_DIR="${root:-$(dirname "$FILE")}"
if [[ -n "$root" && -n "$FILE_REL" && "$FILE_REL" != "$FILE" ]]; then
TYPOS_ARG="$FILE_REL"
fi
Expand Down Expand Up @@ -240,7 +207,7 @@ fi
# judgment was made. Surface the diagnostic via additionalContext (NOT stderr —
# an advisory hook's exit-0 stderr can trip a false "Hook Error" label). Record
# as "skipped" (typos never ran to judgment), the same status as the
# no-config / no-binary paths.
# no-binary path.
hook::ctx_reset
hook::ctx_append "typos-format: typos failed for $(basename "$FILE") (no diagnostics; tool break, not a finding):"
while IFS= read -r line; do
Expand Down
Loading
Loading