Skip to content
Closed
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
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 a file, unconditionally",
"default": true
}
}
Expand Down
19 changes: 19 additions & 0 deletions plugins/typos-format/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,25 @@
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 edited file, matching `markdown-format`'s
unconditional pattern instead of `ruff-format`'s config-gated one. typos
ships a built-in spelling dictionary and runs with zero configuration; the
ancestor walk-up looking for a governing `typos.toml`/`_typos.toml`/
`.typos.toml`/`Cargo.toml`/`pyproject.toml` was only ever an activation
switch the hook implemented itself — it made the hook a silent no-op on any
repo without a hand-authored typos config, defeating the plugin's
zero-config auto-fix purpose. typos' own config discovery is unaffected: a
governing config, when present, is still found and honored by typos itself
(for allowlist/exclude purposes) — the hook just no longer gates activation
on one existing. This is a behavior change for existing installs — a repo
with no typos config now has its files actively rewritten on edit instead
of being left untouched — hence the minor version bump.

## [0.1.0]

### Added
Expand Down
41 changes: 21 additions & 20 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 runs unconditionally — typos ships a built-in spelling dictionary and
needs no configuration to be useful. 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 to widen
its allowlist or excludes — but no such config is required for the plugin to
do its job.

## 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.
- **Unconditional.** typos runs on **every** edited file, whether or not the
repo has a typos config. A config, when present, is discovered by typos
itself (not this hook) to widen the allowlist/excludes — it is never an
activation switch.
- **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,11 @@ 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).

A typos config in the repo is optional — typos runs with its built-in
dictionary either way; a config only widens what it allows or excludes.

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 All @@ -79,10 +80,10 @@ Then verify prerequisites with `/typos-format:setup check`.

## Configuration

The rules themselves are never configured here — they come from the typos
config already in your repository, which the plugin reads automatically. To
change the rules (allowlist a false positive, ignore a pattern), edit that
file.
The rules themselves are never configured here — typos runs with its
built-in dictionary by default, and if your repository has a typos config,
typos reads it automatically to widen the allowlist or excludes. To change
the rules (allowlist a false positive, ignore a pattern), edit that file.

One `userConfig` option tunes the hook itself:

Expand Down
76 changes: 23 additions & 53 deletions plugins/typos-format/hooks/typos-format.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@
# 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
# from PATH only — never downloaded (typos is a standalone Rust binary with no
# per-repo dependency-manager convention, unlike ruff's .venv).
# Unconditional: typos ships a built-in spelling dictionary and runs with zero
# configuration, so this hook runs on every edited file regardless of whether
# the repo has a typos config — matching markdown-format's unconditional
# pattern rather than ruff-format's config-gated one (Ruff/Biome require
# project-specific configuration to be useful; typos does not). When a typos
# config (typos.toml, _typos.toml, .typos.toml, Cargo.toml with
# [workspace.metadata.typos]/[package.metadata.typos], or pyproject.toml with
# [tool.typos]) IS present, typos discovers and honors it itself — per
# crate-ci/typos' own docs at
# https://github.com/crate-ci/typos/blob/master/docs/reference.md — to widen
# its allowlist or excludes; this hook does no config discovery of its own.
# 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).
#
# Hook-precision: this convention is fleet-wide by intent (its owner doc says
# "every plugin hook follows" it), even though today's CI-audited enforcement
Expand Down Expand Up @@ -82,8 +85,8 @@ 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
# the schema-required repo-relative path in data.file.
# Resolve repo root early — used to compute the schema-required repo-relative
# path in data.file and to anchor RUN_DIR below.
REPO_ROOT="$(hook::repo_root "$(dirname "$FILE")")"
# Repo-relative path: schema requires "relative to the consuming repo root".
# On Windows Git Bash, git rev-parse --show-toplevel returns a drive-letter path
Expand Down Expand Up @@ -122,52 +125,20 @@ emit_skipped() {
exit 0
}

# Resolve the file's directory in `pwd` form once. Both walks below start here.
# Resolve the file's directory in `pwd` form once — feeds RUN_DIR/TYPOS_ARG below.
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,9 +149,8 @@ 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 (if any) governs, regardless of nesting depth. Falls back to
# the absolute path when the repo root did not resolve or the file is outside it.
TYPOS_ARG="$FILE"
RUN_DIR="${root:-$FILE_DIR_POSIX}"
if [[ -n "$root" && -n "$FILE_REL" && "$FILE_REL" != "$FILE" ]]; then
Expand Down Expand Up @@ -240,7 +210,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