Mod Bisect Tool
—2026 Update: The project has expanded beyond Fabric to support (Neo)Forge under the name Mod Bisect Tool. It now features a native cross-platform Graphical User Interface built with Gio UI, and the search algorithm has been extended to IMCS-I to handle indeterminate test results.
Website & Downloads | Source Code
Anyone who has run a large Minecraft modpack has been there. The game crashes, the log is unhelpful, and you have a hundred mods to blame. The usual approach is to manually disable half your mods, try again, and repeat until you find the culprit. It works, but it is tedious and easy to get wrong. I wrote this tool to automate that process properly.
The tool supports Fabric, Quilt, and (Neo)Forge (including Sinytra Connector and Kilt configurations). Prebuilt binaries with automatic OS detection and comprehensive documentation are hosted on the project website, and the source code is on GitHub. Step-by-step guides for both versions are available online: GUI User Guide and TUI User Guide.
How It Works
The core of the tool is a bisection search. It splits the pool of candidate mods in half, asks you to run the game and report whether the issue occurred, then eliminates whichever half is clean. Each round cuts the search space in half, so even a modpack with hundreds of mods converges to a result in a handful of steps. The user interface walks you through each test one at a time, so there is no guesswork involved.
One thing that makes this more than a simple binary search is dependency handling. Mods often require other mods to be present, and naively enabling a subset of mods can produce crashes that are unrelated to the actual conflict. The tool includes a dependency resolver that automatically activates any required dependencies when a mod is selected for testing, keeping each test valid. It also reads bundled JARs so that if a conflict is caused by a library shipped inside another mod, the tool still points to the right culprit.
Some conflicts only surface when two or more mods are active together. The bisection engine handles this by continuing to narrow down the candidate set until it isolates the exact combination responsible. After a conflict is found, the tool can set those mods aside and resume searching the rest of the pool, finding any number of separate, unrelated conflicts in one session.
The algorithm described in the appendix (IMCS-I) is my own approach, designed specifically for this use case. It is optimized for situations where only a small number of conflicts exist within a large set of components, focusing on minimizing the number of required test runs by isolating one conflicting element at a time and handling secondary failures that produce indeterminate test outcomes. In contrast, delta debugging (ddmin) and QuickXplain (QXP) have a lot of overhead in such scenarios, but perform better when the number of conflicts is large.
Managing the Search
The tool gives you fine-grained control over which mods participate in the search. A mod can be force-enabled if it must always be present for the issue to reproduce, force-disabled if you already know it is safe to exclude, or omitted to remove it from the candidate pool while still allowing the resolver to activate it as a dependency for other mods. A history page logs every test and its outcome so you can review how the tool arrived at its conclusion, and a live log page exposes internal diagnostics for bug reports.
The tool also supports a dependency override file for cases where a mod's metadata is missing or incorrect. Overrides can be placed next to the executable, in the Minecraft config folder, or left to the tool's own built-in list, with a clear priority order between them.
Graphical User Interface (GUI)
While the terminal user interface (TUI) provides power-user tools like keyboard shortcuts, a full mod management table, a test history page, and an internal log viewer, these features add unnecessary friction for everyday troubleshooting. The GUI is built to streamline the entire experience around what actually matters: isolating the broken mod with as few clicks and decisions as possible.
The GUI is built using Gio UI (gioui / gogio), an immediate-mode GUI library written in pure
Go. Using Gio keeps the binary lightweight and dependency-free while rendering natively across operating systems:
- Windows: Standalone executable (
.exe) with native file dialogs. - macOS: Standalone application bundle (
.app). - Linux: Portable, single-file
AppImage.
Guided Pre-Search Wizard
Instead of burying candidate configuration inside nested menus, the GUI introduces a guided wizard right after folder setup:
- Handling Already-Disabled Mods: Modpacks often contain mods that were disabled intentionally or temporarily. The GUI identifies these upfront and lets you decide whether they should remain permanently disabled throughout the entire bisect or participate in the candidate pool.
- Excluding (Omitting) Safe or Disruptive Mods: You can exclude mods you already know are safe, or mods that actively get in the way of testing. A prime example is Crash Assistant: it displays an interactive GUI dialog whenever the game crashes, which slows down the search since frequent game crashes are an expected part of the bisection process. Omitted mods are removed from the candidate pool, but the dependency resolver can still activate them if another mod requires them.
- Selecting Mods to Keep Enabled: Some issues only reproduce when a specific prerequisite mod is active. For example, if a visual glitch only occurs with shaders enabled, the shader mod (such as Iris) must remain forced-enabled. If it were disabled during testing, you would not be able to tell whether the issue was resolved or simply impossible to observe.
Appendix
Algorithm: Iterative Minimal Conflict Search with Indeterminate Results (IMCS-I)
The Iterative Minimal Conflict Search (IMCS) algorithm is a novel (citation needed hahaha), highly efficient method for identifying a 1-minimal conflict set
from a larger collection of components. Building upon the core principles of binary search and iterative component isolation, IMCS maintains stable and
predictable O(p log n) performance. While sharing the same optimal theoretical complexity as QuickXplain (QXP), IMCS distinguishes itself
by adopting a "lean start" strategy, precisely targeting individual conflict elements and avoiding QXP's upfront speculative tests. This results in superior practical
performance and significantly lower variance for sparse problems, making IMCS ideally suited for real-world troubleshooting scenarios.
In practice, test outcomes are not always binary (GOOD vs. FAIL). A secondary issue, such as an undeclared dependency crashing the
game before the target bug can be observed, leads to an INDETERMINATE result. IMCS-I extends IMCS to resolve these
secondary conflicts automatically during bisection.
1. Objective
To efficiently identify a 1-minimal conflict set of size p from a larger superset of n components (C_all). A conflict
set is defined as the smallest subset of components that causes a system failure (or a designated undesirable outcome) when tested together.
2. Core Principle & Indeterminate Handling
The IMCS algorithm operates on a "lean start, iterative isolation" principle. It fundamentally avoids the high overhead of speculative testing on large component sets. Instead, it executes a series of independent binary searches, each tasked with identifying exactly one new conflict element that contributes to the system's failure.
An INDETERMINATE result on StableSet ∪ C₁ indicates an undeclared dependency: a component in C₁ silently requires a
component in C₂, and the split separated them. Because IMCS guarantees that test(StableSet ∪ CandidateSet) = FAIL on every recursive
call, an INDETERMINATE outcome on StableSet ∪ C₁ guarantees that test((StableSet ∪ C₂) ∪ C₁) = FAIL for free without
requiring an extra test run. When testing C₂ confirms it is clean (GOOD), folding C₂ into StableSet suppresses the secondary conflict across the entire recursive descent into C₁.
3. Algorithm Description
The algorithm consists of a main procedure, FindConflictSet, and a recursive helper, FindNextConflictElement.
Definitions:
- C_all
- The initial superset of all
ncomponents. - ConflictSet
- The set of components confirmed to be part of the minimal conflict set.
- CandidateSet
- The set of components currently being considered for inclusion in the
ConflictSet. - StableSet
- A set of components that has been tested together in the current search context and found to be stable (i.e., does not cause a failure). This set serves as the baseline for subsequent tests.
- test(S)
- A black-box function that returns
FAILif the system exhibits the target failure when configured with setS,GOODif clean, andINDETERMINATEif a secondary issue masks the observation.
Implicit Definitions:
- ClearedSet
- An implicit subset of StableSet (specifically,
StableSet\ConflictSet) comprising components that have been tested and found not to contribute to the current system failure in their respective search contexts.
Main Procedure: FindConflictSet
function FindConflictSet(C_all):
ConflictSet ← {}
CandidateSet ← C_all
loop indefinitely:
// Find the next single component that, in conjunction with the current ConflictSet, contributes to the failure.
nextElement ← FindNextConflictElement(StableSet=ConflictSet, CandidateSet=CandidateSet)
// If no additional conflict element can be found, the process is complete.
if nextElement is null:
break
// Add the found element to the confirmed ConflictSet and remove it from the candidate pool.
ConflictSet ← ConflictSet ∪ {nextElement}
CandidateSet ← CandidateSet \ {nextElement}
// Optimization: Test if the current ConflictSet is already a complete, minimal set.
// If it causes failure, we can terminate early without searching for more components.
if test(ConflictSet) is FAIL:
break
return ConflictSet Helper Procedure: FindNextConflictElement
function FindNextConflictElement(StableSet, CandidateSet):
// Base Case 1: No more candidates to test.
if CandidateSet is empty:
return null
// Base Case 2: Handles CandidateSet of size 1.
if size(CandidateSet) = 1:
let c be the single element in CandidateSet
if test(StableSet ∪ {c}) is FAIL:
return c
else:
// GOOD: not a conflict element.
// INDETERMINATE: c itself causes a secondary conflict; treat as non-element for this search.
return null
// Recursive Step: Divide and conquer.
Split CandidateSet into two halves, C₁ and C₂.
result₁ ← test(StableSet ∪ C₁)
if result₁ is FAIL:
if size(C₁) = 1:
return the single element in C₁
return FindNextConflictElement(StableSet, C₁)
if result₁ is GOOD:
new_stable ← StableSet ∪ C₁
if size(C₂) = 1:
if test(new_stable ∪ C₂) is FAIL:
return the single element in C₂
else:
return null
return FindNextConflictElement(new_stable, C₂)
// --- INDETERMINATE: C₁ has a split-induced secondary conflict ---
if result₁ is INDETERMINATE:
result₂ ← test(StableSet ∪ C₂)
// Primary conflict element is in C₂. Proceed normally.
if result₂ is FAIL:
if size(C₂) = 1:
return the single element in C₂
return FindNextConflictElement(StableSet, C₂)
// C₂ is confirmed clean. Fold it into StableSet and recurse into C₁ (no extra test needed).
if result₂ is GOOD:
return FindNextConflictElement(StableSet ∪ C₂, C₁)
// Both halves are INDETERMINATE (independent secondary conflicts on both sides).
// Search both branches, each suppressing the other's secondary conflict.
if result₂ is INDETERMINATE:
// A practical alternative in this case is to just halt
found ← FindNextConflictElement(StableSet ∪ C₂, C₁)
if found is not null:
return found
return FindNextConflictElement(StableSet ∪ C₁, C₂) 4. Handling INDETERMINATE Outcomes
Single-INDETERMINATE: Resolving with a Single Test
When a test on C₁ returns INDETERMINATE, an undeclared dependency inside C₁ was separated from its required mod in C₂. A naive troubleshooting approach might spend up to O(log n) additional tests trying to hunt down and restore the missing dependency.
IMCS-I avoids this entirely. Because the algorithm already guarantees that test(StableSet ∪ CandidateSet) = FAIL on entry, we only need a single extra test on C₂ (test(StableSet ∪ C₂)) to know exactly how to proceed:
- If
C₂returnsFAIL: The primary conflict element resides inC₂. Recursion proceeds intoC₂as normal. - If
C₂returnsGOOD:C₂is confirmed clean, meaning(StableSet ∪ C₂) ∪ C₁is guaranteed to fail. We foldC₂intoStableSetto satisfyC₁'s missing dependency for the rest of the descent—requiring zero further tests.
Double-INDETERMINATE: Preventing Complexity Explosion
If C₂ also returns INDETERMINATE, two independent undeclared dependencies cross the split in opposite directions. Neither
half has a clean baseline to lean on.
If the algorithm attempted to fork and search both branches, the recurrence relation would shift from T(n) = T(n/2) + O(1) to T(n) = 2·T(n/2) + O(1), causing the worst-case time complexity to explode from O(log n) to O(n). Because this situation
is extremely rare, the tool's implementation avoids exponential branch explosion by simply halting the search, reporting the two conflicting groups,
and letting the user resolve the missing dependency before resuming.
5. Complexity Analysis
Time Complexity: O((p + q) log n)
The algorithm's total cost is dominated by the p calls to the FindNextConflictElement procedure. Each call performs a binary search
on a diminishing set of candidates (from n down to n - p + 1), with a baseline cost of O(log n) tests. When secondary
conflicts occur, each isolated single-INDETERMINATE event costs exactly one extra complement test before clean logarithmic recursion resumes.
Across p conflict elements and q single-indeterminate occurrences, the total test complexity is tightly bounded at O((p + q) log n).
6. Extension: Finding All Independent Conflicts (IMCS-Enumerator)
The core IMCS algorithm finds a single conflict set. The IMCS_Enumerator is a meta-procedure that extends this to discover all independent minimal
conflict sets in a system that may have multiple unrelated faults.
A persistent, cross-iteration test cache (KnowledgeBase) is not used. Such a cache is unworkable in practice for two fundamental reasons:
- A
FAILresult is only relevant to its specific set of components; once a conflict element from that set is found and removed, that exact test can never be run again, rendering the cached result useless. - A
GOODresult is context-dependent on the user's current focus; caching it could incorrectly mask a different, independent issue in a subsequent search.
Therefore, the only knowledge that can be safely persisted between iterations is the reduction of the candidate pool itself (CandidateSet \ newConflictSet).
Meta-Procedure: IMCS_Enumerator
function IMCS_Enumerator(C_all):
AllConflictSets ← []
CandidateSet ← C_all
loop indefinitely:
// Find the next conflict set using a fresh IMCS run. This ensures that
// knowledge from previous runs does not incorrectly influence the current search.
newConflictSet ← FindConflictSet(CandidateSet)
// If IMCS returns an empty set, no more conflicts exist among the candidates.
if newConflictSet is empty:
break
// A new independent conflict has been found.
add newConflictSet to AllConflictSets
// The only safe and persistent knowledge transfer is shrinking the problem space
// by removing the components of the just-found conflict.
CandidateSet ← CandidateSet \ newConflictSet
return AllConflictSets 7. Capabilities and Limitations
The IMCS algorithm suite is highly optimized for a specific class of diagnostic problems.
Capabilities & Strengths
- Optimized for Sparse Conflicts: Exceptional
O(p log n)performance and low variance when finding a small number (p) of conflict elements within a large set (n). This makes it ideal for real-world troubleshooting. - Robust Indeterminate Handling: IMCS-I automatically resolves secondary issues caused by split-induced missing dependencies without discarding progress.
- Black-Box Operation: IMCS requires no internal knowledge of the system being tested. It operates purely on the
GOOD/FAILoutcome of tests, making it universally applicable. - Low Overhead & High Stability: The "lean start" strategy avoids wasteful speculative tests, and the iterative nature results in extremely stable, predictable performance with minimal variance, as confirmed by benchmarks.
- Complete Conflict Enumeration: The
IMCS_Enumeratormeta-procedure enumerates all separate, unrelated conflict sets efficiently.
Limitations
- Finding Non-Minimal Supersets: Designed to find only the smallest set causing a failure (
1-minimal); does not report larger sets containing non-essential components. - Conflict Prioritization: The algorithm finds conflict sets in an order determined by the binary search path, not by any measure of severity or probability.
- Dense Conflicts: Less efficient for dense problems where
pis a large fraction ofn, where algorithms likeQuickXplainthat leverage information reuse may perform better. - Non-Deterministic Systems: Assumes deterministic outcomes. If identical component configurations produce contradictory results, IMCS may terminate with an incorrect conflict set.
- Side Effects & External State: The algorithm relies on mod toggling being a pure, reversible operation. If enabling or disabling a mod causes persistent side effects in game settings, the search can fail. This is quite rare in practice.