plandex-ai/plandex · error

panic in gitRemoveIndexLockFileIfExists: %v %s

Error message

panic in gitRemoveIndexLockFileIfExists: %v
%s

What it means

gitRemoveIndexLockFileIfExists spawns one goroutine per lock-file path, each with a recover() that converts any panic into this error (including the stack trace via debug.Stack()) sent to errCh. You see this error when one of the per-path goroutines panicked — almost always a nil-pointer/nil-map or similar bug in the removal path rather than a git or filesystem condition. The recover ensures the panic doesn't crash the process; runtime.Goexit() prevents a double-send to the channel.

Source

Thrown at app/server/db/git.go:620

}

func gitRemoveIndexLockFileIfExists(repoDir string) error {
	log.Printf("[Git] gitRemoveIndexLockFileIfExists - repoDir: %s", repoDir)

	paths := []string{
		filepath.Join(repoDir, ".git", "index.lock"),
		filepath.Join(repoDir, ".git", "refs", "heads", "HEAD.lock"),
		filepath.Join(repoDir, ".git", "HEAD.lock"),
	}

	errCh := make(chan error, len(paths))

	for _, path := range paths {
		go func(path string) {
			defer func() {
				if r := recover(); r != nil {
					log.Printf("panic in gitRemoveIndexLockFileIfExists: %v\n%s", r, debug.Stack())
					errCh <- fmt.Errorf("panic in gitRemoveIndexLockFileIfExists: %v\n%s", r, debug.Stack())
					runtime.Goexit() // don't allow outer function to continue and double-send to channel
				}
			}()
			if err := removeLockFile(path); err != nil {
				errCh <- err
				return
			}
			errCh <- nil
		}(path)
	}

	errs := []error{}
	for i := 0; i < len(paths); i++ {
		err := <-errCh
		if err != nil {
			errs = append(errs, err)
		}
	}

View on GitHub (pinned to e2d772072e)

Solutions

  1. Read the stack trace appended to the error message — it pinpoints the exact panicking function and line.
  2. Fix the underlying nil pointer / bad type assertion at that line; this error is a symptom, not the root cause.
  3. If caused by a recent change, diff removeLockFile / gitRemoveIndexLockFileIfExists against the last working version.
  4. Confirm inputs are valid (non-empty repoDir that contains a .git directory) before invoking write operations.

Example fix

// before: unchecked assumption inside goroutine work
idx := someMap["key"].(string) // panics if missing
// after
idx, ok := someMap["key"].(string)
if !ok {
    errCh <- fmt.Errorf("missing key in map")
    return
}
Defensive patterns

Strategy: try-catch

Validate before calling

if repoDir == "" {
    return errors.New("repoDir must be non-empty")
}
if fi, err := os.Stat(filepath.Join(repoDir, ".git")); err != nil || !fi.IsDir() {
    return fmt.Errorf("%s is not a git repository", repoDir)
}

Type guard

func isPanicErr(err error) bool {
    return err != nil && strings.Contains(err.Error(), "panic in gitRemoveIndexLockFileIfExists")
}

Try / catch

if err := gitCommit(repoDir, msg); err != nil {
    if isPanicErr(err) {
        log.Printf("recovered panic in lock cleanup, stack: %s", err)
        return fmt.Errorf("internal bug in lock cleanup: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: Any panic inside the deferred-recover-protected goroutine in gitRemoveIndexLockFileIfExists while it processes .git/index.lock, .git/refs/heads/HEAD.lock, or .git/HEAD.lock — e.g. a nil error value being dereferenced or channel misuse reaching the goroutine.

Common situations: A code change introduced a nil dereference or unchecked type assertion in removeLockFile/its helpers; running under unusual runtime conditions (stack exhaustion) surfaces latent panics; modified forked code passes invalid arguments (e.g. empty repoDir producing weird paths, though normally not a panic).

Related errors


AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05). Data as JSON: /api/errors/495d41a4f1459fb2. Report an issue: GitHub.