Convert Finder SKILL.md to Preprocessor Script
SkillDev toolsConvert 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.
No other account needed.
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-XXXXSKILL.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:
| Pattern | Discovery Method | Has FUNC_XREFS | Has LLM_DECOMPILE | Has INHERIT_VFUNCS | Has FUNC_VTABLE_RELATIONS | preprocess_skill has llm_config |
|---|---|---|---|---|---|---|
| A — Regular function via xref strings | find_regex + xrefs_to on debug strings | Yes | No | No | No | No |
| B — Virtual function via xref strings | Same as A, but function is in a vtable | Yes | No | No | Yes | No |
| C — Virtual function via LLM_DECOMPILE | Decompile a known predecessor function, identify vfunc call offsets | No | Yes | No | Yes | Yes |
| D — Regular function via LLM_DECOMPILE | Decompile a known predecessor function, identify direct call targets | No | Yes | No | No | Yes |
| E — Struct member offset via LLM_DECOMPILE | Decompile a known predecessor function, identify struct field access offsets | No | Yes | No | No | Yes |
| F — Virtual function via INHERIT_VFUNCS | Inherit vtable slot index from a known base-class vfunc, look up same slot in derived-class vtable | No | No | Yes | No | No |
| G — ConCommand handler function | Find the handler callback registered via RegisterConCommand by matching command name and help string | No (uses COMMAND_NAME/HELP_STRING) | No | No | No | No |
| H — Secondary (ordinal) vtable | Locate a class's secondary vtable via mangled symbol (Windows) or offset-to-top (Linux) | No | No | No | No | No |
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:
- Target function names — all functions the skill identifies (may be 1 or many)
- Target struct member names — all struct member offsets the skill identifies (e.g.
CCheckTransmitInfo_m_nPlayerSlot) - Discovery method for each target:
- Does it use
find_regex/xrefs_towith 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)
- Does it use
- Function category —
func(regular),vfunc(virtual, has vtable slot),structmember, orvtable - VTable class name — if virtual, e.g.
CBaseEntity,CBasePlayerPawn,INetworkMessages - Xref strings — debug strings used in
find_regexpatterns (for xref-string patterns). Check if these differ between Windows and Linux — if so, you need platform-specificFUNC_XREFS_WINDOWS/FUNC_XREFS_LINUX. Use theFULLMATCH: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"). - Predecessor function — the function whose decompiled code reveals the target (for LLM_DECOMPILE patterns)
- 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)
- 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_vafield) - 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_NAMESinstead ofTARGET_FUNCTION_NAMES - Passes
struct_member_names=instead offunc_names=topreprocess_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(notfuncorvfunc)
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, orFUNC_VTABLE_RELATIONS - Uses
inherit_vfuncs=parameter instead offunc_names= - No
llm_configparameter inpreprocess_skill - configs/.yaml
expected_inputmust 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