Browse documentation

Patch an operation

An Infix patches an operation inside a method: a call, property access, field read/write, constructor, or literal load. It uses the familiar prefixes, postfixes, and finalizers. Other calls to the same member are unchanged.

The containing method is the outer method. For calls, the method being called is the inner method. For less common cases, see Limits and recipes and instruction edits.

Infix · a patch around a selected operation
Outer method · Outer.Run()
Earlier instructions
Selected operation · Helper.Decide(value)
  1. Inner prefixBefore this operation
  2. OperationCall, read, write, create, or load
  3. Inner postfixAfter this operation
Inner finalizerHandles this selected operation's patch sequence.
Later instructions

The orange border marks the selection. Calls from other methods are unaffected by this Infix. If an exception escapes the selection, normal exception handling in the outer method decides what happens next.

A working example

This patch changes calls to Helper.Decide inside Outer.Run. value is the inner argument; [HarmonyOuter] int mode reads the outer argument.

public static class Helper
{
    [MethodImpl(MethodImplOptions.NoInlining)]
    public static bool Decide(string value)
    {
        DecidePatch.Events.Add("call: " + value);
        return value.Length > 0;
    }
}

public static class Outer
{
    [MethodImpl(MethodImplOptions.NoInlining)]
    public static bool Run(string value, int mode) => Helper.Decide(value);
}

[HarmonyPatch(typeof(Outer), nameof(Outer.Run))]
public static class DecidePatch
{
    public static readonly List<string> Events = [];

    [HarmonyInfix(typeof(Helper), nameof(Helper.Decide), typeof(string))]
    [HarmonyPrefix, HarmonyPriority(Priority.High)]
    public static bool Before(ref string value, ref bool __result, [HarmonyOuter] int mode)
    {
        Events.Add("high priority");
        if (mode == 0)
        {
            __result = false;
            return false;
        }
        value += ".";
        return true;
    }

    [HarmonyInfix(typeof(Helper), nameof(Helper.Decide), typeof(string))]
    [HarmonyPrefix, HarmonyPriority(Priority.Low)]
    static void Observe(bool __runOriginal) => Events.Add("low priority: " + __runOriginal);

    [HarmonyInfix(typeof(Helper), nameof(Helper.Decide), typeof(string))]
    [HarmonyPostfix]
    static void After(bool __result, bool __runOriginal) => Events.Add("postfix: " + __result + ", " + __runOriginal);
}

Install it with your Harmony instance:

public static void Install(Harmony harmony) => harmony.CreateClassProcessor(typeof(DecidePatch)).Patch();

Outer.Run("hello", 1) produces:

high priority
low priority: True
call: hello.
postfix: True, True

For Outer.Run("hello", 0), the first prefix returns false. The call is skipped, but the observation-only prefix and postfix still run:

high priority
low priority: False
postfix: False, False

Selecting calls

Select the outer method on the patch class with [HarmonyPatch], TargetMethod, or TargetMethods. Give each Infix callback a [HarmonyInfix] and its role, such as [HarmonyPrefix]. Do not also put [HarmonyPatch] on that callback.

For calls, supply the declaring type, method name, and argument types needed to identify an overload. The usual ArgumentType[] variations handle ref, out, and pointer parameters.

Positions chooses which matches to patch:

  • Omitted or empty: all matches.
  • Positions = new[] { 2 }: the second match.
  • Positions = new[] { -1 }: the last match.
  • Repeated positions: that match once per registration.

Positions are one-based, so 0 is invalid. Explicitly setting Positions = null is also invalid. Installation fails if a requested position is outside the matches, or if the target has no matches at all. All Infixes count from the same instructions after ordinary transpilers, before any Infixes are inserted. Another Infix cannot shift these positions. A game update or transpiler can. See Be careful with indices.

Exact targets and generic families

Container<int>.Use selects only that construction, not Container<string>.Use.

To select every Container<T>.Use, use the member from Container<>. Likewise, a generic method definition selects all its constructions. Opening the declaring type does not also open the method's generic arguments, or vice versa.

The patch must work at every selected call: object value can observe an int or a string, but ref int value cannot edit both. Positions count all family matches together. Other partially open targets with free parameters are rejected.

If the attribute cannot identify a generic overload, pass its MethodInfo manually.

Manual registration

Use AddInnerPrefix, AddInnerPostfix, or AddInnerFinalizer:

public static void InstallManual(Harmony harmony)
{
    var prefix = new HarmonyMethod(typeof(DecidePatch), nameof(DecidePatch.Before))
    {
        innerMethod = new InnerMethod(AccessTools.Method(typeof(Helper), nameof(Helper.Decide)))
    };
    harmony.CreateProcessor(AccessTools.Method(typeof(Outer), nameof(Outer.Run))).AddInnerPrefix(prefix).Patch();
}

Every AddInner... call adds a registration, including repeated methods. It does not replace existing patches. A processor retains its configuration, so calling Patch() again adds those registrations again. Unpatching by method or owner removes all matching registrations.

The MethodInfo overload reads [HarmonyInfix] from the callback. Alternatively, supply HarmonyMethod.innerMethod or innerTarget. If both forms are present, their targets and positions must agree. Registered inputs are copied; editing them later has no effect.

Patch callbacks must be static, non-generic methods on non-generic types. Dynamic patch methods and patch factories are not supported.

Properties, fields, constructors, and literals

Use one of these selectors on each callback, together with its prefix, postfix, or finalizer attribute:

[HarmonyInfix(typeof(Thing), "Value", InnerTargetKind.Getter)]
[HarmonyInfix(typeof(Thing), "Value", InnerTargetKind.Setter)]
[HarmonyInfix(typeof(Thing), "count", InnerTargetKind.FieldRead)]
[HarmonyInfix(typeof(Thing), "count", InnerTargetKind.FieldWrite)]
[HarmonyInfix(typeof(StringBuilder), InnerTargetKind.Constructor)]
[HarmonyInfix("StatsReport_FinalValue", Positions = new[] { -1 })]

For indexers, argument types describe the index parameters, not the setter's value. For constructors, omitted argument types mean the parameterless constructor.

Operation Arguments Result and instance
Property access Accessor arguments Same as calling the accessor
Field read None Field value; instance for instance fields
Field write value or __0 No result; instance for instance fields
Construction (newobj) Constructor arguments New object or struct; no incoming __instance
Literal load None Loaded value; no instance

For example, ref int value changes a field write, and ref int __result changes a field read. A constructor postfix receives the new value through __result. Skipping construction also skips allocation, but its arguments have already been evaluated.

For manual registration, create an InnerTarget from the method, property, field, or constructor. Use InnerTarget.Constant(value) for literals.

Literal selectors accept non-null string, int, long, float, or double. Types must match; floating-point values match by their exact bits. Choose distinctive values: 0 may occur in many unrelated expressions, and compiler folding can remove a source constant entirely.

Capture at one operation, use at another

Use [HarmonyOuter] __var_name to save a value for another Infix in the same patch class:

[HarmonyPatch(typeof(Report), nameof(Report.Build))]
public static class ReportPatch
{
    [HarmonyPostfix, HarmonyInfix(typeof(StringBuilder), InnerTargetKind.Constructor)]
    static void Capture(StringBuilder __result, [HarmonyOuter] out StringBuilder __var_builder)
        => __var_builder = __result;

    [HarmonyPostfix, HarmonyInfix("StatsReport_FinalValue")]
    static void AddName([HarmonyOuter] string name, [HarmonyOuter] StringBuilder __var_builder)
        => __var_builder?.Append(name).Append(": ");
}

Each outer invocation gets its own default-initialized slots. Handle the default if the reader can run without the writer. Use more names for more values. A later postfix can replace a result you captured; see captured results.

Ordinary patches cannot use these named slots. To share state with an ordinary patch, use its __state and request [HarmonyOuter] __state in the Infix.

Ordering, skipping, and exceptions

Infix follows ordinary Harmony ordering, including priority and before/after rules. Prefixes and postfixes are ordered independently, so postfix order does not automatically reverse prefix order. Exact and generic-family patches join the same lists at each selected operation.

Without dependency overrides, high- and low-priority prefixes and void postfixes run like this:

high prefix → low prefix → call → high postfix → low postfix

Returning postfixes run after void postfixes. As with ordinary pass-through postfixes, their first parameter receives the previous result. For Infix, that parameter and the return type must exactly match the operation's result type. Method-valued results such as MethodInfo are valid too.

A prefix returning false skips the operation and later prefixes that can affect it. Observation-only prefixes still run, following the ordinary prefix rules. Postfixes run after a completed or skipped operation, but an exception stops the remaining prefixes/postfixes.

By-value bool __runOriginal reports whether this operation runs. It does not report whether patches on the called method skip that method's body.

Inner finalizers

Finalizers run after success or failure. Observe __exception with a void finalizer, or return an exception to preserve/replace it. Return null to suppress it.

public static class Parser
{
    [MethodImpl(MethodImplOptions.NoInlining)]
    public static int Parse(string text) => int.Parse(text);
}

public static class RecoveringOuter
{
    [MethodImpl(MethodImplOptions.NoInlining)]
    public static int Total(string text) => 5 + Parser.Parse(text);
}

[HarmonyPatch(typeof(RecoveringOuter), nameof(RecoveringOuter.Total))]
public static class RecoverPatch
{
    [HarmonyFinalizer, HarmonyInfix(typeof(Parser), nameof(Parser.Parse), typeof(string))]
    static Exception Recover(Exception __exception, ref int __result)
    {
        if (__exception is not FormatException) return __exception;
        __result = 0;
        return null;
    }
}

Install RecoverPatch with CreateClassProcessor(...).Patch(). Total("3") returns 8; Total("invalid") returns 5. Recovery covers this operation's prefixes, call, and postfixes, not earlier argument evaluation or later outer code. An exception left afterward reaches the outer handlers.

Ordinary finalizer subtleties still apply, including re-entry after a finalizer throws and which result is visible after a postfix fails. See Limits.

Arguments and scope

Arguments are evaluated once, before the first Infix. All patches at that operation share them.

If the call takes int value, an Infix's ref int value changes what the call receives, not the variable that supplied it. If the call itself takes ref int value, writes reach that original storage. Use [HarmonyOuter] ref int value to change the outer argument explicitly.

Injection Default inner scope [HarmonyOuter] scope
Named argument, __N, HarmonyArgument Call argument Outer argument
__instance Call receiver Outer receiver
___field Field on the call receiver's type Field on the outer type
__originalMethod Called method or constructor, including generic arguments; unavailable for fields and literals Outer method
__originalMember Called method, constructor, or field; unavailable for literals Outer method
__args Mutable call arguments Mutable outer arguments
__result, __resultRef Result or ref-return replacement Unavailable
__runOriginal Whether the operation runs Unavailable
__state This patch type's state for this call execution Shared with this patch type's ordinary outer state
__var_N Unavailable Original outer local N
__var_name Unavailable This patch type's named local for the outer invocation
Harmony delegate Resolve against the call receiver Resolve against the outer receiver
__exception By-value exception in an inner finalizer Unavailable

Inner __state resets for each execution, including loop iterations. Prefixes, postfixes, and finalizers from the same patch type share it at that site. Shared state and named locals must agree on their type.

For a real argument named __result, use [HarmonyArgument("__result", ArgumentMode.Original)]. This performs exact, case-sensitive argument lookup in the selected scope, bypassing all special names.

[HarmonyOuter] is Infix-only. Harmony never falls back to the other scope.

Mutable argument arrays

object[] __args writes back changed elements even without ref. Replacing the whole array with ref object[] is not supported. The receiver is not part of either array.

You can request both inner and outer arrays when the inner call has no ref, out, or in parameters, or the outer method has no arguments. Otherwise their write-backs might disagree about the same storage, so Harmony rejects the combination. An array combined with a typed writer or boxed copy-back can cause the same conflict.

Use typed by-value parameters to observe one scope, or direct typed refs to edit both. Distinct slots and typed observations remain valid. Do not retain __args for future calls. Values that cannot be boxed, such as pointers and Span<T>, need typed parameters.

Supported calls and failures

Normal call and callvirt instructions keep their dispatch behavior and any patches on the callee. Concrete struct calls, including constrained. instance calls, are supported too.

Some operations cannot be targets: indirect calls (calli), constructor initialization via call, tail calls, varargs, field-address loads, readonly field writes, and instance fields on structs. Static fields on structs and readonly field reads are supported. Typed pointers and byref-like values work without boxing; C# delegate* signatures do not. See Limits for details.

A failed installation leaves the previous wrapper intact, rather than installing part of the new patch set. Inspection and unpatching include all three inner roles.

An older Harmony that cannot read installed Infix state refuses to rebuild it. Removing the last Infix restores ordinary-patch state. This does not let a binary requiring new API types run against old-only Harmony. For duplicate assembly identities and recovery, see loader limits.

When multiple Harmony assemblies update the same method, apply those updates one at a time. Do not update it from its own prepare or transpiler callbacks.

Generated bodies and captured variables

Iterator and async bodies usually run in a generated MoveNext method. Opt in with OuterBody = InfixOuterBody.Auto on [HarmonyInfix], or infixOuterBody = InfixOuterBody.Auto on HarmonyMethod. The default patches the declared method. Auto leaves ordinary methods unchanged.

This selects one body, not its helper methods or whole call graph. A finalizer on a call returning a task handles that call's synchronous exception, not a later task failure.

Use ArgumentMode.Captured to access a live source variable stored in a compiler-generated field:

public static class Sequence
{
    public static IEnumerable<int> Count(int limit)
    {
        while (limit > 0) yield return Visit(limit--);
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    public static int Visit(int value) => value;
}

[HarmonyPatch(typeof(Sequence), nameof(Sequence.Count))]
public static class SequencePatch
{
    [HarmonyPrefix, HarmonyInfix(typeof(Sequence), nameof(Sequence.Visit), typeof(int), OuterBody = InfixOuterBody.Auto)]
    static void LimitRemaining(
        [HarmonyOuter, HarmonyArgument("limit", ArgumentMode.Captured)] ref int remaining)
        => remaining = Math.Min(remaining, 1);
}

Sequence.Count(5) now yields 5, 1: the first call's argument was already evaluated, but changing the live limit affects the next iteration. Captured fields survive yields; Harmony's ordinary __state and __var_name locals reset on each MoveNext invocation.

Captured lookup is Infix-only, explicit, and case-sensitive. It uses the inner generated receiver or closure arguments; add [HarmonyOuter] for variables in the outer generated body. Missing, ambiguous, or optimized-away variables cannot be recovered.

For explicit targets, use AccessTools.StateMachineMoveNext, AccessTools.LocalFunction, or AccessTools.Lambdas. Lambda order is not a stable identity across builds. Direct inspection and unpatching use the resolved generated method; processor and owner-wide unpatching find it automatically.

Keep patch-owned values across await and yield

Use [HarmonyOuter, HarmonyArgument("name", ArgumentMode.Persistent)] for a value that belongs to the whole async call or enumeration. It starts at default, survives suspensions, and is separate for concurrent calls and separate enumerators. The name is scoped to the actual patch declaring type. Prefixes, postfixes, and finalizers in that type can share it; all bindings must agree on its type and lifetime.

Persistent state · one async call or enumeration
One generated execution
  1. RunA selected patch reads or updates its named value.
  2. SuspendAt a yield or an await that suspends execution.
  3. ResumeA later patch in the same type sees the saved value.
ArgumentMode.Persistent · the same named slot across suspensions

Each separate call or enumerator gets its own value, starting at default. Ordinary outer __state and named locals start again on each MoveNext invocation. The persistent-state limits describe supported compiler protocols and cleanup.

This patch makes Sequence.Count(3) yield 1, 2, 3. Enumerating again starts at 1:

[HarmonyPatch(typeof(Sequence), nameof(Sequence.Count))]
public static class SequencePersistentPatch
{
    [HarmonyPrefix, HarmonyInfix(typeof(Sequence), nameof(Sequence.Visit), typeof(int), OuterBody = InfixOuterBody.Auto)]
    static void NumberVisit(ref int value,
        [HarmonyOuter, HarmonyArgument("visits", ArgumentMode.Persistent)] ref int visits)
        => value = ++visits;
}

The same binding works in an async method selected with OuterBody = InfixOuterBody.Auto, including calls on either side of an await. Use ref or out to replace a slot's value and a value parameter to read it. Use several names for several values. Ordinary synchronous methods keep the value for one invocation.

Harmony releases its stored references when the execution completes, faults, finishes cancellation, or the iterator is disposed. Removing the last persistent patch from a body also releases its saved state. This does not call Dispose on objects you store. See persistent-state limits for supported compiler protocols, cleanup, and live-update behavior.

Optional patch-body inlining and authoring recipes

[HarmonyInline] asks Harmony to copy a small patch body into the Infix wrapper. Unsupported bodies keep a normal call. Enable patch debugging or Harmony.DEBUG to see why it falls back.

Inlining can change stack traces and is not a promise of faster code. Measure it. If you later change Harmony patches on the callback itself, rebuild the outer method to refresh it.

See recipes and instruction edits for owner groups and CodeMatcher examples. Public InlineSignature helps transpilers inspect calli signatures; it does not make function pointers Infix targets.

Harmony 3 preview

For the stable release, read the 2.x documentation.