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

  1. 1
    Find violations. ESLint's JSON output lists every file with at least one violation of the rule you pass in.
  2. 2
    Fix in parallel. Each file gets its own droid exec --auto low run, so a bad edit stays contained to one file. CONCURRENCY sets how many run at once.
  3. 3
    Verify. The script runs ESLint again on each file. Files that still break the rule are reported, and the script exits non-zero.
  4. 4
    Review. 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_KEY is 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 (at warn or error). The script only sees violations that ESLint reports, so a rule that's turned off looks like a clean codebase.
  • jq is on your PATH.

The script

Save this as droid-fix-lint.sh and make it executable with chmod +x droid-fix-lint.sh.

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:

Bash
DRY_RUN=true ./droid-fix-lint.sh @typescript-eslint/no-floating-promises src

Then fix them. Use the rule ID exactly as ESLint reports it, including any plugin prefix such as @typescript-eslint/:

Bash
./droid-fix-lint.sh @typescript-eslint/no-floating-promises src
The lint autofix script listing three files with no-floating-promises violations, then reporting each one fixed (sped up)

In this run, Droid awaited the promises inside an async function and marked the call in a synchronous handler with void:

Diff
 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;
 }
Diff
 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:

lint-guide.md
# 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.
Bash
GUIDE_FILE=lint-guide.md ./droid-fix-lint.sh \
  @typescript-eslint/no-floating-promises src

Open a pull request

Review the changes, run your usual checks, then open one pull request for the rule:

Bash
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 --fill

Tips

  • Fix one rule per run and per pull request. Mixed rules make the diff hard to review.
  • Raise CONCURRENCY gradually. 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.