# 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. **Find violations.** ESLint's JSON output lists every file with at least one violation of the rule you pass in.
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. **Verify.** The script runs ESLint again on each file. Files that still break the rule are reported, and the script exits non-zero.
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](/droid-exec/overview#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`.

```bash title="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)](/docs-assets/images/droid-exec/recipes/lint-autofix.gif)

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`:

```markdown title="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.

<RelatedLinks>
  <RelatedLink href="/droid-exec/overview" title="Droid Exec">
    Flags, autonomy levels, and output formats for headless runs.
  </RelatedLink>
  <RelatedLink href="/droid-exec/recipes/organize-imports" title="Organize imports">
    Normalize imports across every package in a monorepo.
  </RelatedLink>
</RelatedLinks>
