Convert Finder SKILL.md to Preprocessor Script

SkillDev tools

Convert an existing find-XXXX SKILL.md into a preprocessor Python script, updating configs/<GAMEVER>.yaml and removing the old SKILL.md. Covers xref-string-based and LLM_DECOMPILE-based discovery patterns.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Convert Finder SKILL.md to Preprocessor Script skill

What this skill tells your AI

The instructions your AI receives, as published by hlnd2t/cs2_vibesignatures in .claude/skills/convert-finder-skill-to-preprocessor-scripts/SKILL.md and read by ahel’s review.

Port an existing .claude/skills/find-XXXX/SKILL.md into an ida_preprocessor_scripts/find-XXXX.py preprocessor script, update configs/<GAMEVER>.yaml entries, and delete the old SKILL.md.

When to Use

  • A find-XXXX SKILL.md exists in .claude/skills/ and needs to be converted to a preprocessor script
  • The SKILL.md uses either xref-string search (find_regex / xrefs_to) or decompile-based vtable analysis to discover functions

Overview

Eight preprocessor patterns exist. The SKILL.md's discovery method and target type determine which to use:

PatternDiscovery MethodHas FUNC_XREFSHas LLM_DECOMPILEHas INHERIT_VFUNCSHas FUNC_VTABLE_RELATIONSpreprocess_skill has llm_config
A — Regular function via xref stringsfind_regex + xrefs_to on debug stringsYesNoNoNoNo
B — Virtual function via xref stringsSame as A, but function is in a vtableYesNoNoYesNo
C — Virtual function via LLM_DECOMPILEDecompile a known predecessor function, identify vfunc call offsetsNoYesNoYesYes
D — Regular function via LLM_DECOMPILEDecompile a known predecessor function, identify direct call targetsNoYesNoNoYes
E — Struct member offset via LLM_DECOMPILEDecompile a known predecessor function, identify struct field access offsetsNoYesNoNoYes
F — Virtual function via INHERIT_VFUNCSInherit vtable slot index from a known base-class vfunc, look up same slot in derived-class vtableNoNoYesNoNo
G — ConCommand handler functionFind the handler callback registered via RegisterConCommand by matching command name and help stringNo (uses COMMAND_NAME/HELP_STRING)NoNoNoNo
H — Secondary (ordinal) vtableLocate a class's secondary vtable via mangled symbol (Windows) or offset-to-top (Linux)NoNoNoNoNo

Additionally, struct member offsets can be mixed into any pattern as a secondary target (see "Struct Member Mixin" section below).


Step 1: Read and Analyze the SKILL.md

Read the target .claude/skills/find-XXXX/SKILL.md.

Extract:

  1. Target function names — all functions the skill identifies (may be 1 or many)
  2. Target struct member names — all struct member offsets the skill identifies (e.g. CCheckTransmitInfo_m_nPlayerSlot)
  3. Discovery method for each target:
    • Does it use find_regex / xrefs_to with debug strings? → xref-string based (Patterns A/B)
    • Does it search for a ConCommand registration (command name + help string) and extract the handler callback? → ConCommand handler (Pattern G)
    • Does it load a predecessor YAML, decompile that function, and extract vfunc offsets / struct offsets from code patterns? → LLM_DECOMPILE based (Patterns C/D/E)
    • Is the target a derived-class override of a known base-class vfunc (same vtable slot, different class)? → INHERIT_VFUNCS based (Pattern F)
    • Does the SKILL.md locate a secondary vtable using a mangled symbol name (Windows @@6B@_0) or offset-to-top (Linux)? → ordinal vtable (Pattern H)
  4. Function categoryfunc (regular), vfunc (virtual, has vtable slot), structmember, or vtable
  5. VTable class name — if virtual, e.g. CBaseEntity, CBasePlayerPawn, INetworkMessages
  6. Xref strings — debug strings used in find_regex patterns (for xref-string patterns). Check if these differ between Windows and Linux — if so, you need platform-specific FUNC_XREFS_WINDOWS / FUNC_XREFS_LINUX. Use the FULLMATCH: prefix (e.g. "FULLMATCH:Precache") when you need exact-string matching instead of substring matching — this prevents false positives when the target string is short or generic (e.g. "Precache", "userid", "team").
  7. Predecessor function — the function whose decompiled code reveals the target (for LLM_DECOMPILE patterns)
  8. Base vfunc for inheritance — if the target is a derived-class override of a known base-class vfunc, the base vfunc name (for INHERIT_VFUNCS pattern)
  9. Dependencies — which existing YAMLs are needed as inputs (vtable YAMLs, predecessor function YAMLs, base vfunc YAMLs)

Step 2: Plan the Split

If the SKILL.md discovers multiple functions using different methods or from different starting points, split them into separate preprocessor scripts. Each script handles one "discovery unit" — a group of functions findable from the same method and starting point.

Same script: Functions found from the same xref string, or from the same decompiled reference. Separate scripts: Functions found by xref strings vs. functions found by decompiling one of those xref-found functions.

CRITICAL — LLM_DECOMPILE dependency chains: When LLM_DECOMPILE targets form a chain (FuncA → FuncB → FuncC, where each is the predecessor of the next), they MUST be in separate scripts — one script per link in the chain. A single script CANNOT handle chained LLM_DECOMPILE predecessors because:

  • The LLM_DECOMPILE fallback resolves the predecessor's address from its output YAML (func_va field)
  • Within a single script run, FuncB's output YAML doesn't exist yet when FuncC's LLM_DECOMPILE tries to use FuncB as predecessor
  • The IDA name-lookup fallback also fails because the predecessor wasn't renamed in IDA yet

Rule of thumb: If target X's LLM_DECOMPILE references target Y as predecessor, and Y is also discovered by LLM_DECOMPILE (not xref strings), then X and Y MUST be in different scripts with a configs/.yaml dependency chain.

Example split (what we did for CBaseEntity_TakeDamageOld):

  • Script 1: find-CBaseEntity_TakeDamageOld.py — finds TakeDamageOld via xref string (Pattern A)
  • Script 2: find-CBaseEntity_OnTakeDamage.py — finds OnTakeDamage by decompiling TakeDamageOld (Pattern C)
  • Script 3: find-CBaseEntity_OnTakeDamage_Alive-AND-Dying-AND-Dead.py — finds 3 vfuncs by decompiling OnTakeDamage (Pattern C)

Step 3: Generate the Preprocessor Script(s)

Script location: ida_preprocessor_scripts/find-{skill_name}.py

The filename MUST match the name field in configs/<GAMEVER>.yaml skill entry.

Pattern A — Regular function via xref strings

Use when: function is non-virtual, discovered via debug string cross-references.

#!/usr/bin/env python3
"""Preprocess script for find-{SKILL_NAME} skill."""

from ida_analyze_util import preprocess_common_skill

TARGET_FUNCTION_NAMES = [
    "{FUNC_NAME}",
]

FUNC_XREFS = [
    {
        "func_name": "{FUNC_NAME}",
        "xref_strings": [
            "{XREF_STRING_1}",  # Debug string from SKILL.md's find_regex pattern
        ],
        "xref_gvs": [],          # global variable names if needed, usually empty
        "xref_signatures": [],    # byte patterns if needed, usually empty
        "xref_funcs": [],         # known caller function names if needed
        "exclude_funcs": [],      # function names to exclude from results
        "exclude_strings": [],    # strings to exclude
        "exclude_gvs": [],        # global variable names to exclude
        "exclude_signatures": [], # byte patterns to exclude
    },
]

GENERATE_YAML_DESIRED_FIELDS = [
    # (symbol_name, generate_yaml_fields)
    (
        "{FUNC_NAME}",
        [
            "func_name",
            "func_sig",
            "func_va",
            "func_rva",
            "func_size",
        ],
    ),
]

async def preprocess_skill(
    session, skill_name, expected_outputs, old_yaml_map,
    new_binary_dir, platform, image_base, debug=False,
):
    """Reuse previous gamever func_sig to locate target function(s) and write YAML."""
    return await preprocess_common_skill(
        session=session,
        expected_outputs=expected_outputs,
        old_yaml_map=old_yaml_map,
        new_binary_dir=new_binary_dir,
        platform=platform,
        image_base=image_base,
        func_names=TARGET_FUNCTION_NAMES,
        func_xrefs=FUNC_XREFS,
        generate_yaml_desired_fields=GENERATE_YAML_DESIRED_FIELDS,
        debug=debug,
    )

Pattern B — Virtual function via xref strings

Use when: function IS virtual (has vtable slot), but discovered via debug string cross-references.

Same as Pattern A, but adds FUNC_VTABLE_RELATIONS and vtable fields to GENERATE_YAML_DESIRED_FIELDS:

#!/usr/bin/env python3
"""Preprocess script for find-{SKILL_NAME} skill."""

from ida_analyze_util import preprocess_common_skill

TARGET_FUNCTION_NAMES = [
    "{FUNC_NAME}",
]

FUNC_XREFS = [
    {
        "func_name": "{FUNC_NAME}",
        "xref_strings": [
            "{XREF_STRING_1}",
        ],
        "xref_gvs": [],
        "xref_signatures": [],
        "xref_funcs": [],
        "exclude_funcs": [],
        "exclude_strings": [],
        "exclude_gvs": [],
        "exclude_signatures": [],
    },
]

FUNC_VTABLE_RELATIONS = [
    # (func_name, vtable_class)
    ("{FUNC_NAME}", "{VTABLE_CLASS}"),
]

GENERATE_YAML_DESIRED_FIELDS = [
    # (symbol_name, generate_yaml_fields)
    (
        "{FUNC_NAME}",
        [
            "func_name",
            "func_va",
            "func_rva",
            "func_size",
            "func_sig",
            "vtable_name",
            "vfunc_offset",
            "vfunc_index",
        ],
    ),
]

async def preprocess_skill(
    session, skill_name, expected_outputs, old_yaml_map,
    new_binary_dir, platform, image_base, debug=False,
):
    """Reuse previous gamever func_sig to locate target function(s) and write YAML."""
    return await preprocess_common_skill(
        session=session,
        expected_outputs=expected_outputs,
        old_yaml_map=old_yaml_map,
        new_binary_dir=new_binary_dir,
        platform=platform,
        image_base=image_base,
        func_names=TARGET_FUNCTION_NAMES,
        func_xrefs=FUNC_XREFS,
        func_vtable_relations=FUNC_VTABLE_RELATIONS,
        generate_yaml_desired_fields=GENERATE_YAML_DESIRED_FIELDS,
        debug=debug,
    )

Platform-Specific Xref Strings (Patterns A & B variant)

When xref strings differ between Windows and Linux (e.g. Windows has full ClassName::Method assertion strings while Linux has only ./filename.cpp:linenum), split into two variables:

FUNC_XREFS_WINDOWS = [
    {
        "func_name": "{FUNC_NAME}",
        "xref_strings": [
            "CSource2GameEntities::CheckTransmit",  # Full assertion string on Windows
        ],
        "xref_gvs": [], "xref_signatures": [], "xref_funcs": [],
        "exclude_funcs": [], "exclude_strings": [], "exclude_gvs": [], "exclude_signatures": [],
    },
]

FUNC_XREFS_LINUX = [
    {
        "func_name": "{FUNC_NAME}",
        "xref_strings": [
            "./gameinterface.cpp:30",  # Shorter path-based string on Linux
        ],
        "xref_gvs": [], "xref_signatures": [], "xref_funcs": [],
        "exclude_funcs": [], "exclude_strings": [], "exclude_gvs": [], "exclude_signatures": [],
    },
]

Then in preprocess_skill, use a ternary to select the right one:

        func_xrefs=FUNC_XREFS_WINDOWS if platform == "windows" else FUNC_XREFS_LINUX,

This applies to both Pattern A and Pattern B — the only change is replacing the single FUNC_XREFS with the platform-specific pair.

Pattern C — Virtual function via LLM_DECOMPILE

Use when: function IS virtual (has vtable slot), discovered by decompiling a known predecessor function and reading vfunc call offsets from the decompiled code.

IMPORTANT — func_va in output YAMLs: If this function will be used as a predecessor by a downstream LLM_DECOMPILE script (i.e., another script decompiles this function to find further targets), you MUST include func_va, func_rva, and func_size in GENERATE_YAML_DESIRED_FIELDS. The downstream script resolves the predecessor's address by reading func_va from the output YAML. Without it, the LLM_DECOMPILE fallback fails with "failed to resolve llm_decompile target function address". When in doubt, always include func_va — it never hurts.

#!/usr/bin/env python3
"""Preprocess script for find-{SKILL_NAME} skill."""

from ida_analyze_util import preprocess_common_skill

TARGET_FUNCTION_NAMES = [
    "{FUNC_NAME_1}",
    # "{FUNC_NAME_2}",  # Add more if the skill finds multiple functions from the same reference
]

LLM_DECOMPILE = [
    # (symbol_name, path_to_prompt, path_to_reference)
    # ONE entry per target function. All entries sharing the same reference
    # YAML will be resolved from the same decompiled predecessor code.
    (
        "{FUNC_NAME_1}",
        "prompt/call_llm_decompile.md",
        "references/{MODULE}/{PREDECESSOR_FUNC}.{platform}.yaml",
    ),
    (
        "{FUNC_NAME_2}",
        "prompt/call_llm_decompile.md",
        "references/{MODULE}/{PREDECESSOR_FUNC}.{platform}.yaml",
    ),
    # ... one entry per target function, all pointing to the same reference
]

FUNC_VTABLE_RELATIONS = [
    # (func_name, vtable_class)
    ("{FUNC_NAME_1}", "{VTABLE_CLASS}"),
    ("{FUNC_NAME_2}", "{VTABLE_CLASS}"),
    # ... one entry per target function
]

GENERATE_YAML_DESIRED_FIELDS = [
    # (symbol_name, generate_yaml_fields)
    # Include func_va/func_rva/func_size if this function is a predecessor for downstream LLM_DECOMPILE
    (
        "{FUNC_NAME_1}",
        [
            "func_name",
            "func_va",
            "func_rva",
            "func_size",
            "vfunc_sig",
            "vfunc_offset",
            "vfunc_index",
            "vtable_name",
        ],
    ),
    (
        "{FUNC_NAME_2}",
        [
            "func_name",
            "func_va",
            "func_rva",
            "func_size",
            "vfunc_sig",
            "vfunc_offset",
            "vfunc_index",
            "vtable_name",
        ],
    ),
    # ... one entry per target function
]

async def preprocess_skill(
    session, skill_name, expected_outputs, old_yaml_map,
    new_binary_dir, platform, image_base, llm_config=None, debug=False,
):

    """Reuse previous gamever func_sig to locate target function(s) and write YAML."""
    return await preprocess_common_skill(
        session=session,
        expected_outputs=expected_outputs,
        old_yaml_map=old_yaml_map,
        new_binary_dir=new_binary_dir,
        platform=platform,
        image_base=image_base,
        func_names=TARGET_FUNCTION_NAMES,
        func_vtable_relations=FUNC_VTABLE_RELATIONS,
        llm_decompile_specs=LLM_DECOMPILE,
        llm_config=llm_config,
        generate_yaml_desired_fields=GENERATE_YAML_DESIRED_FIELDS,
        debug=debug,
    )

Pattern D — Regular function via LLM_DECOMPILE

Use when: function is NOT virtual, discovered by decompiling a known predecessor function and identifying direct call targets (not vtable-based calls) from the decompiled code.

#!/usr/bin/env python3
"""Preprocess script for find-{SKILL_NAME} skill."""

from ida_analyze_util import preprocess_common_skill

TARGET_FUNCTION_NAMES = [
    "{FUNC_NAME}",
]

LLM_DECOMPILE = [
    # (symbol_name, path_to_prompt, path_to_reference)
    (
        "{FUNC_NAME}",
        "prompt/call_llm_decompile.md",
        "references/{MODULE}/{PREDECESSOR_FUNC}.{platform}.yaml",
    ),
]

GENERATE_YAML_DESIRED_FIELDS = [
    # (symbol_name, generate_yaml_fields)
    (
        "{FUNC_NAME}",
        [
            "func_name",
            "func_sig",
            "func_va",
            "func_rva",
            "func_size",
        ],
    ),
]

async def preprocess_skill(
    session, skill_name, expected_outputs, old_yaml_map,
    new_binary_dir, platform, image_base, llm_config=None, debug=False,
):
    """Reuse previous gamever func_sig to locate target function(s) and write YAML."""
    return await preprocess_common_skill(
        session=session,
        expected_outputs=expected_outputs,
        old_yaml_map=old_yaml_map,
        new_binary_dir=new_binary_dir,
        platform=platform,
        image_base=image_base,
        func_names=TARGET_FUNCTION_NAMES,
        llm_decompile_specs=LLM_DECOMPILE,
        llm_config=llm_config,
        generate_yaml_desired_fields=GENERATE_YAML_DESIRED_FIELDS,
        debug=debug,
    )

Pattern E — Struct member offset via LLM_DECOMPILE

Use when: target is a struct member offset (not a function), discovered by decompiling a known predecessor function and identifying struct field access patterns (e.g. *(int *)(ptr + 0x240)).

#!/usr/bin/env python3
"""Preprocess script for find-{SKILL_NAME} skill."""

from ida_analyze_util import preprocess_common_skill

TARGET_STRUCT_MEMBER_NAMES = [
    "{STRUCT_MEMBER_NAME}",  # e.g. "CCheckTransmitInfo_m_nPlayerSlot"
]

LLM_DECOMPILE = [
    # (symbol_name, path_to_prompt, path_to_reference)
    (
        "{STRUCT_MEMBER_NAME}",
        "prompt/call_llm_decompile.md",
        "references/{MODULE}/{PREDECESSOR_FUNC}.{platform}.yaml",
    ),
]

GENERATE_YAML_DESIRED_FIELDS = [
    # (symbol_name, generate_yaml_fields)
    (
        "{STRUCT_MEMBER_NAME}",
        [
            "struct_name",
            "member_name",
            "offset",
            "size",
            "offset_sig",
            "offset_sig_disp",
        ],
    ),
]

async def preprocess_skill(
    session, skill_name, expected_outputs, old_yaml_map,
    new_binary_dir, platform, image_base, llm_config=None, debug=False,
):
    """Reuse previous gamever offset_sig to locate target struct offset and write YAML."""
    return await preprocess_common_skill(
        session=session,
        expected_outputs=expected_outputs,
        old_yaml_map=old_yaml_map,
        new_binary_dir=new_binary_dir,
        platform=platform,
        image_base=image_base,
        struct_member_names=TARGET_STRUCT_MEMBER_NAMES,
        llm_decompile_specs=LLM_DECOMPILE,
        llm_config=llm_config,
        generate_yaml_desired_fields=GENERATE_YAML_DESIRED_FIELDS,
        debug=debug,
    )

Key differences from Pattern D:

  • Uses TARGET_STRUCT_MEMBER_NAMES instead of TARGET_FUNCTION_NAMES
  • Passes struct_member_names= instead of func_names= to preprocess_common_skill
  • YAML fields are struct-specific: struct_name, member_name, offset, size, offset_sig, offset_sig_disp
  • No FUNC_VTABLE_RELATIONS
  • configs/.yaml symbol category is structmember (not func or vfunc)

Pattern F — Virtual function via INHERIT_VFUNCS

Use when: the target is a derived-class override of a known base-class virtual function. The base vfunc has already been found (by another script), and this script inherits its vtable slot index to look up the same slot in the derived class's vtable.

This is the simplest pattern — no xref strings, no LLM decompilation needed. Just a vtable slot lookup.

#!/usr/bin/env python3
"""Preprocess script for find-{SKILL_NAME} skill."""

from ida_analyze_util import preprocess_common_skill

INHERIT_VFUNCS = [
    # (target_func_name, inherit_vtable_class, base_vfunc_name, generate_func_sig)
    ("{DERIVED_FUNC_NAME}", "{DERIVED_VTABLE_CLASS}", "{BASE_VFUNC_NAME}", True),
]

GENERATE_YAML_DESIRED_FIELDS = [
    # (symbol_name, generate_yaml_fields)
    (
        "{DERIVED_FUNC_NAME}",
        [
            "func_name",
            "func_va",
            "func_rva",
            "func_size",
            "func_sig",
            "vtable_name",
            "vfunc_offset",
            "vfunc_index",
        ],
    ),
]

async def preprocess_skill(
    session,
    skill_name,
    expected_outputs,
    old_yaml_map,
    new_binary_dir,
    platform,
    image_base,
    debug=False,
):
    """Reuse old func_sig first; fallback to vtable index + generated signature when needed."""
    _ = skill_name

    return await preprocess_common_skill(
        session=session,
        expected_outputs=expected_outputs,
        old_yaml_map=old_yaml_map,
        new_binary_dir=new_binary_dir,
        platform=platform,
        image_base=image_base,
        inherit_vfuncs=INHERIT_VFUNCS,
        generate_yaml_desired_fields=GENERATE_YAML_DESIRED_FIELDS,
        debug=debug,
    )

INHERIT_VFUNCS tuple fields:

  • target_func_name — name for the derived-class function (e.g. "CBaseEntity_Precache")
  • inherit_vtable_class — class whose vtable to look up (e.g. "CBaseEntity")
  • base_vfunc_name — YAML artifact stem of the base-class vfunc that defines the slot index (e.g. "CEntityInstance_Precache"). Can be cross-module: "../engine/INetworkMessages_FindNetworkGroup"
  • generate_func_sig — (optional, default True) whether to generate a func_sig if no old YAML exists

Key differences from other patterns:

  • No TARGET_FUNCTION_NAMES, FUNC_XREFS, LLM_DECOMPILE, or FUNC_VTABLE_RELATIONS
  • Uses inherit_vfuncs= parameter instead of func_names=
  • No llm_config parameter in preprocess_skill
  • configs/.yaml expected_input must include both the base vfunc YAML and the derived class vtable YAML
  • configs/.yaml symbol category is vfunc

Pattern G — ConCommand handler function

Use when: the SKILL.md searches for a ConCommand registration (e.g. find_regex pattern="bot_kill.*all"xrefs_to → handler callback). The target is the handler function registered via RegisterConCommand, identified by matching the command name string and/or help string.

This pattern uses a dedicated helper (_registerconcommand.py) instead of preprocess_common_skill. It scans for the exact command name and help string in the binary's string table, finds xrefs to those strings, locates nearby RegisterConCommand calls, and recovers the handler function pointer from the call arguments.

#!/usr/bin/env python3
"""Preprocess script for find-{SKILL_NAME} skill."""

from ida_preprocessor_scripts._registerconcommand import (
    preprocess_registerconcommand_skill,
)


TARGET_FUNCTION_NAMES = [
    "{HANDLER_NAME}",
]

COMMAND_NAME = "{command_name}"
HELP_STRING = (
    "{help_string_part1}"
    "{help_string_part2}"  # Split long strings across lines for readability
)
SEARCH_WINDOW_BEFORE_CALL = 96
SEARCH_WINDOW_AFTER_XREF = 96

GENERATE_YAML_DESIRED_FIELDS = [
    (
        "{HANDLER_NAME}",
        [
            "func_name",
            "func_sig",
            "func_va",
            "func_rva",
            "func_size",
        ],
    ),
]


async def preprocess_skill(
    session,
    skill_name,
    expected_outputs,
    old_yaml_map,
    new_binary_dir,
    platform,
    image_base,
    debug=False,
):
    _ = skill_name, old_yaml_map
    return await preprocess_registerconcommand_skill(
        session=session,
        expected_outputs=expected_outputs,
        new_binary_dir=new_binary_dir,
        platform=platform,
        image_base=image_base,
        target_name=TARGET_FUNCTION_NAMES[0],
        generate_yaml_desired_fields=GENERATE_YAML_DESIRED_FIELDS,
        command_name=COMMAND_NAME,
        help_string=HELP_STRING,
        rename_to=TARGET_FUNCTION_NAMES[0],
        search_window_before_call=SEARCH_WINDOW_BEFORE_CALL,
        search_window_after_xref=SEARCH_WINDOW_AFTER_XREF,
        debug=debug,
    )

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
65
Forks
10
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
convert-finder-skill-to-preprocessor-scripts
Source
github.com/hlnd2t/cs2_vibesignatures