peckphp/peck
Peck is a fast CLI tool that finds spelling and wording mistakes across your PHP codebase—filenames, class, method and property names, docs, and more. Powered by GNU Aspell, it fits naturally into your workflow like Pint or Pest. Requires PHP 8.2+.
1. Install the system dependency first — this is not optional.
Peck shells out to GNU Aspell, so Composer alone won't make it work:
# Debian/Ubuntu (also the basis for WSL)
sudo apt-get install aspell aspell-en
# macOS
brew install aspell
# Windows: use WSL, or Scoop
scoop install main/aspell
Verify with aspell dump dicts — if that command fails, nothing else will work either.
2. Install as a dev dependency and scaffold config.
composer require peckphp/peck --dev
./vendor/bin/peck --init
--init writes a peck.json in your project root. Open it immediately; this file is the whole control surface of the tool.
3. Look at peck.json before looking at output. A useful starting config is three keys:
{
"preset": "laravel",
"language": "en_US",
"ignore": {
"words": ["config", "namespace"],
"paths": ["vendor", "node_modules", "storage", "bootstrap/cache"]
}
}
The laravel preset pre-seeds common framework vocabulary so you don't hand-list middleware, eloquent, etc. yourself.
4. Baseline on the first run.
./vendor/bin/peck --ignore-all
A mature codebase will produce hundreds of hits on run one. --ignore-all writes them into the config in one pass so you start from green. Review the diff, commit it, and from then on you only see new mistakes. That ratchet is the actual day-to-day value of the tool.
5. Your first real use case: wire it next to Pint and Pest as composer test:typos and run it locally before pushing. Don't start by blocking CI — publish the count first.
Composer script, advisory by default.
{
"scripts": {
"test:typos": "peck"
}
}
Run composer test:typos manually, or chain it into a broader composer test alongside Pint and Pest. Keeping it a named script means CI and local dev use the identical invocation — no divergence between "works on my machine" and the runner.
CI: install Aspell explicitly, then run. Peck is not a self-contained binary; every job needs the system package. The README's GitHub Actions pattern is the one to copy:
- name: Install Aspell
shell: bash
run: |
if [[ "$RUNNER_OS" == "Linux" ]]; then
sudo apt-get update && sudo apt-get install -y aspell aspell-en
elif [[ "$RUNNER_OS" == "macOS" ]]; then
brew install aspell
fi
- name: Check Typos
shell: bash
run: |
if [[ "$RUNNER_OS" == "Linux" || "$RUNNER_OS" == "macOS" ]]; then
composer test:typos
fi
Keep the run step non-blocking initially (continue-on-error: true or a separate non-required job). Promote to a required check only once the ignore list has stopped churning for a couple of weeks.
The ratchet workflow. This is the pattern that makes Peck survivable on inherited code:
--ignore-all, commit.Scope discipline with ignore.paths and --path. Because the default path is now the project root (changed in v0.1.3), make sure vendor/, node_modules/, build output, and generated fixtures are ignored. For targeted runs on a single area:
./vendor/bin/peck --path app/Domain/Payments
This is also your debugging tool — run it on one directory or one file to isolate a confusing report.
Use --text for short strings. Commit messages, PR titles, or a proposed method name can be checked without a file:
./vendor/bin/peck --text "getRecieveAdress"
Handy as a quick "is this identifier actually spelled correctly" reflex before you commit to a public API name.
Monorepos and multiple configs. --config relative/path/to/peck.json lets one repository carry separate configs per package or app, each with its own preset and ignore list. Run Peck once per config in CI rather than trying to build one giant shared list.
Language switching. Set "language": "en_GB" (or de, fr, etc.) to match your team's spelling convention — but only after confirming the dictionary is installed via aspell dump dicts. Do this early: en_US vs en_GB disagreements produce false positives on every occurrence of colour/colour, behaviour, initialize/initialise.
Pre-commit feedback. Run Peck via a pre-commit hook (Captain Hook, Husky, or a plain .git/hooks/pre-commit) so developers see results in seconds rather than after a CI round trip. Feedback-loop ownership is the difference between a check developers tolerate and one they route around.
Aspell is the real adoption tax. Composer resolves the package fine; the tool then fails at runtime if Aspell is absent. Docker images, freshly provisioned CI runners, and Windows machines are the usual suspects. Add the install step to your dev container image, not just to CI, or onboarding will silently break.
Windows without WSL is a second-class path. Scoop works, but the README's own recommendation is WSL. If your team is mixed-platform, validate on a Windows machine early rather than discovering it during a sprint deadline.
Pre-1.0 software. The README states Peck is "not yet ready for production use." Treat that as a pinning instruction: lock a specific version in composer.json and upgrade deliberately, rather than floating on ^0.x and getting a changed default path or preset mid-sprint.
--ignore-all writes to your config — review the diff. It's generous by design and will happily add noise. Check that it hasn't added per-file entries where a single ignore.paths glob would do, and keep the list sorted so merges don't conflict. Assign one owner to peck.json; an unowned ignore list decays into a junk drawer within a quarter.
False positives cluster in specific places:
getHttpReq or S3Client can trip the dictionary. Add the fragment to ignore.words, not the whole identifier.| character was explicitly handled in v0.1.1's formatter, but complex union/intersection type expressions remain a place to check first if a strange report appears.Misspellings with no suggestions are still reported. Since v0.2.0 these are surfaced as misspellings rather than silently skipped — so you may see an error line with no "did you mean" hint. That's expected; add the word to ignore.words or fix it manually.
peck.json is looked up in the project root by default. If you run Peck from a subdirectory or through a wrapper script that changes the working directory, it may not find your config and will fall back to auto-detecting the directory to check. If results suddenly change, pass --config explicitly.
--path overrides the config's path resolution. When debugging a surprising result, re-run with an explicit --path to a single file and compare against the full-project run — the difference usually tells you whether the issue is scope or a genuine false positive.
Extension points are limited to configuration. There is no custom-preset mechanism, no regex rules engine, and no naming-convention policy — Peck does dictionary checking plus word/path ignores, full stop. If you need architectural naming rules, that's PHPStan or a custom PHP-CS-Fixer rule, not Peck. The one lever you do have is upstream: if your organisation's Laravel vocabulary keeps landing in ignore.words, contributing those terms to the laravel preset is cheaper than maintaining a local list forever.
Polyglot repos get partial coverage. Peck targets PHP-adjacent assets and Markdown. TypeScript, Python, or Go files will not get the same treatment — don't advertise it as a repo-wide spell checker if the repo isn't predominantly PHP.
How can I help you explore Laravel packages today?