Lint autofix with Droid Exec
Fix violations of one ESLint rule across a whole codebase in parallel with droid exec, verify each file, and open a pull request with the results.
Use this recipe when eslint --fix can't repair a rule on its own, for example when the fix needs to restructure a function, add a missing wrapper, or replace an unsafe pattern. The script asks ESLint which files break the rule, runs one droid exec per file in parallel, and runs ESLint again on each file to confirm the fix.
How it works
- 1Find violations. ESLint's JSON output lists every file with at least one violation of the rule you pass in.
- 2Fix in parallel. Each file gets its own
droid exec --auto lowrun, so a bad edit stays contained to one file.CONCURRENCYsets how many run at once. - 3Verify. The script runs ESLint again on each file. Files that still break the rule are reported, and the script exits non-zero.
- 4Review. Nothing is committed. You check the diff, run your tests, and open the pull request.
--auto low lets Droid edit project files but not install packages or run git writes, which is all this job needs. The script itself runs ESLint, so Droid doesn't need build or test permissions.
Prerequisites
- The Droid CLI is installed and
FACTORY_API_KEYis set. See Droid Exec installation. - ESLint runs in the repository with
npx eslint, and the rule you want to fix is enabled in your ESLint config (atwarnorerror). The script only sees violations that ESLint reports, so a rule that's turned off looks like a clean codebase. jqis on yourPATH.
The script
Save this as droid-fix-lint.sh and make it executable with chmod +x droid-fix-lint.sh.
#!/usr/bin/env bash
# Fix every violation of one ESLint rule with droid exec, one file per run.
# Usage: ./droid-fix-lint.sh <eslint-rule> [directory]
set -euo pipefail
RULE="${1:?usage: droid-fix-lint.sh <eslint-rule> [directory]}"
TARGET_DIR="${2:-.}"
CONCURRENCY="${CONCURRENCY:-4}"
DRY_RUN="${DRY_RUN:-false}"
GUIDE_FILE="${GUIDE_FILE:-}"
# ESLint exits 1 when it finds problems and 2 when it can't run at all.
status=0
report=$(npx eslint "$TARGET_DIR" --format json) || status=$?
if [ "$status" -gt 1 ]; then
echo "ESLint failed to run (exit $status). Fix the ESLint setup first." >&2
exit "$status"
fi
files=$(echo "$report" | jq -r --arg rule "$RULE" --arg cwd "$(pwd -P)/" '
.[] | select(any(.messages[]; .ruleId == $rule))
| .filePath | ltrimstr($cwd)')
if [ -z "$files" ]; then
echo "No $RULE violations found."
exit 0
fi
echo "Files with $RULE violations:"
echo "$files"
[ "$DRY_RUN" = "true" ] && exit 0
fix_file() {
local file="$1"
local prompt="Fix every violation of the ESLint rule $RULE in $file.
Read the rule's documentation or its source in this repository
if you are unsure what it requires.
Change only what the rule needs. Keep behavior, exports, and types the same.
Do not add eslint-disable comments."
if [ -n "$GUIDE_FILE" ]; then
prompt="$prompt
Follow the before and after examples in $GUIDE_FILE."
fi
droid exec --auto low "$prompt" > /dev/null
local report status=0
report=$(npx eslint "$file" --format json) || status=$?
if [ "$status" -le 1 ] && echo "$report" | jq -e --arg rule "$RULE" \
'[.[].messages[] | select(.ruleId == $rule)] | length == 0' > /dev/null
then
echo "fixed: $file"
else
echo "still failing: $file" >&2
return 1
fi
}
export -f fix_file
export RULE GUIDE_FILE
echo "$files" | tr '\n' '\0' \
| xargs -0 -P "$CONCURRENCY" -I {} bash -c 'fix_file "$1"' _ {}Run it
Start with a dry run to see how many files the rule touches:
DRY_RUN=true ./droid-fix-lint.sh @typescript-eslint/no-floating-promises srcThen fix them. Use the rule ID exactly as ESLint reports it, including any plugin prefix such as @typescript-eslint/:
./droid-fix-lint.sh @typescript-eslint/no-floating-promises srcIn this run, Droid awaited the promises inside an async function and marked the call in a synchronous handler with void:
export async function createUser(input: Partial<User>): Promise<User> {
if (!input.email) throw new HttpError(400, 'Invalid input');
const user: User = { id: crypto.randomUUID(), email: input.email };
- saveUser(user);
- audit('user.created');
+ await saveUser(user);
+ await audit('user.created');
return user;
} export function onClick(label: string) {
- track('click:' + formatName(label));
+ void track('click:' + formatName(label));
}For large codebases, run one package or directory at a time so each pull request stays reviewable.
Give Droid examples
Accuracy improves most when Droid sees the fix you want. Write a short guide file with one or two before and after pairs, plus any cases to leave alone, and pass it with GUIDE_FILE:
# no-floating-promises
Before:
saveUser(user);
After:
await saveUser(user);
In callbacks that can't be async, such as event handlers,
use `void saveUser(user);` instead.
Leave test files that intentionally fire and forget unchanged.GUIDE_FILE=lint-guide.md ./droid-fix-lint.sh \
@typescript-eslint/no-floating-promises srcOpen a pull request
Review the changes, run your usual checks, then open one pull request for the rule:
git checkout -b fix/no-floating-promises
npm run lint && npm run typecheck && npm test
git add -u
git commit -m "fix: resolve no-floating-promises violations"
git push -u origin HEAD
gh pr create --fillTips
- Fix one rule per run and per pull request. Mixed rules make the diff hard to review.
- Raise
CONCURRENCYgradually. Each run is a separate Droid session, so parallel runs also use more tokens at once. - Run your test suite before you open the pull request. ESLint confirms the rule passes, not that behavior is unchanged.