Summary
The wiki already documents an exact way for a mod to tell whether Wh_ModInit
ran before the target began executing or was loaded into a running process —
Development tips → Avoid mod loading on process
startup.
It works, and it is not a heuristic. But it is a hardcoded, per-architecture
offset into an undocumented TEB field, presented as a development-time tip to be
stripped before release.
I'd like the same fact exposed as a supported call — e.g.
BOOL Wh_IsInitialProcessLoad(), or a value the engine sets before
Wh_ModInit — so that mods needing it in released code don't each ship their
own copy of the magic numbers.
Why
Wh_ModInit runs in two very different situations, and for a whole class of
mods they differ in whether the mod can work at all.
Any mod hooking something the process consumes once, early — the command line,
the environment block, CRT initialisation, startup-only registry reads — is
effective only when loaded at process start. Injected into a running process,
the hook installs successfully, reports success, and accomplishes nothing,
because the thing it intercepts was already read.
This isn't a rare edge. It happens on every reinstall: mod install --file
reloads the mod into already-running targets, which is exactly the situation
where such hooks are inert. The mod reads as installed and enabled while
silently doing nothing until the app restarts — a confusing state to debug,
especially for a user who didn't perform the install themselves.
Concretely: my mod hooks GetCommandLineW to add
--remote-debugging-port=<port> so Chromium opens a DevTools port it can then
talk to. On a fresh process start this works. Reloaded into a running app, the
hook is installed but Chromium parsed its command line long ago — so the mod
picks a port, announces it, waits for an endpoint that will never exist, and
eventually gives up. Anything reading that announcement finds a dead port.
Knowing which case it's in, a mod can log an accurate "restart the target for
this to take effect", return FALSE cleanly instead of half-initialising, and
skip setup it knows cannot work.
What the wiki tip already gives, and what it doesn't
The documented check reads SameTebFlags (0x17EE on x64, 0x0FCA on x86) and
tests the 0x0400 initial-thread bit. Under the callback-thread model in
Mod lifetime —
startup load runs on the target's main thread, runtime load runs on the
Windhawk engine thread — that bit answers the question exactly, with no
threshold and no timing assumption.
So this request is not "there's no way to detect this". It's narrower:
- It's undocumented OS internals. The offset is a per-architecture constant
with no contract behind it, and the inference also depends on Windhawk's
current injection mechanics. Both are reasonable for a dev-time guard; both
are uncomfortable in a mod shipped to users.
- The wiki frames it as dev-only, with the opposite polarity. There it means
"bail out on startup load so a crash can't loop". The case above needs the
same check at release time, inverted — bail out on runtime load. Same fact,
opposite use, and the tip explicitly isn't meant to survive to release.
- It's only meaningful on the thread that runs
Wh_ModInit. Elsewhere — a
hook body, Wh_ModSettingsChanged, a worker thread — it tells you nothing, so
it has to be latched into a global at init. Workable, but a supported call
could be a queryable per-load fact.
- Unclear whether it's defined for tool mods at all.
WhTool_ModInit runs in
a Windhawk-hosted process with a different lifecycle; I haven't tested what
the initial-thread bit means there. An engine-provided answer wouldn't have
the ambiguity.
Suggested shape
Either of:
BOOL Wh_IsInitialProcessLoad(); // TRUE when ModInit runs before the process executes
or a value the engine sets before calling Wh_ModInit, alongside the existing
WH_MOD_ID / WH_MOD_VERSION constants.
If this is considered adequately covered by the wiki tip, a reasonable smaller
outcome would be documenting the technique as release-safe (rather than
dev-only) and noting the inverted-polarity use — that alone would remove most of
the hesitation about shipping it.
Related
Pairs naturally with a $requiresRestart annotation on settings (separate
request) — same underlying reality, one at the mod level and one at the
individual-setting level. That one is independent of this: it's a settings-UI
affordance the engine must render, which no amount of mod-side detection can
substitute for.
Tested on 2.0.0-alpha.2.
Summary
The wiki already documents an exact way for a mod to tell whether
Wh_ModInitran before the target began executing or was loaded into a running process —
Development tips → Avoid mod loading on process
startup.
It works, and it is not a heuristic. But it is a hardcoded, per-architecture
offset into an undocumented TEB field, presented as a development-time tip to be
stripped before release.
I'd like the same fact exposed as a supported call — e.g.
BOOL Wh_IsInitialProcessLoad(), or a value the engine sets beforeWh_ModInit— so that mods needing it in released code don't each ship theirown copy of the magic numbers.
Why
Wh_ModInitruns in two very different situations, and for a whole class ofmods they differ in whether the mod can work at all.
Any mod hooking something the process consumes once, early — the command line,
the environment block, CRT initialisation, startup-only registry reads — is
effective only when loaded at process start. Injected into a running process,
the hook installs successfully, reports success, and accomplishes nothing,
because the thing it intercepts was already read.
This isn't a rare edge. It happens on every reinstall:
mod install --filereloads the mod into already-running targets, which is exactly the situation
where such hooks are inert. The mod reads as installed and enabled while
silently doing nothing until the app restarts — a confusing state to debug,
especially for a user who didn't perform the install themselves.
Concretely: my mod hooks
GetCommandLineWto add--remote-debugging-port=<port>so Chromium opens a DevTools port it can thentalk to. On a fresh process start this works. Reloaded into a running app, the
hook is installed but Chromium parsed its command line long ago — so the mod
picks a port, announces it, waits for an endpoint that will never exist, and
eventually gives up. Anything reading that announcement finds a dead port.
Knowing which case it's in, a mod can log an accurate "restart the target for
this to take effect", return
FALSEcleanly instead of half-initialising, andskip setup it knows cannot work.
What the wiki tip already gives, and what it doesn't
The documented check reads
SameTebFlags(0x17EEon x64,0x0FCAon x86) andtests the
0x0400initial-thread bit. Under the callback-thread model inMod lifetime —
startup load runs on the target's main thread, runtime load runs on the
Windhawk engine thread — that bit answers the question exactly, with no
threshold and no timing assumption.
So this request is not "there's no way to detect this". It's narrower:
with no contract behind it, and the inference also depends on Windhawk's
current injection mechanics. Both are reasonable for a dev-time guard; both
are uncomfortable in a mod shipped to users.
"bail out on startup load so a crash can't loop". The case above needs the
same check at release time, inverted — bail out on runtime load. Same fact,
opposite use, and the tip explicitly isn't meant to survive to release.
Wh_ModInit. Elsewhere — ahook body,
Wh_ModSettingsChanged, a worker thread — it tells you nothing, soit has to be latched into a global at init. Workable, but a supported call
could be a queryable per-load fact.
WhTool_ModInitruns ina Windhawk-hosted process with a different lifecycle; I haven't tested what
the initial-thread bit means there. An engine-provided answer wouldn't have
the ambiguity.
Suggested shape
Either of:
or a value the engine sets before calling
Wh_ModInit, alongside the existingWH_MOD_ID/WH_MOD_VERSIONconstants.If this is considered adequately covered by the wiki tip, a reasonable smaller
outcome would be documenting the technique as release-safe (rather than
dev-only) and noting the inverted-polarity use — that alone would remove most of
the hesitation about shipping it.
Related
Pairs naturally with a
$requiresRestartannotation on settings (separaterequest) — same underlying reality, one at the mod level and one at the
individual-setting level. That one is independent of this: it's a settings-UI
affordance the engine must render, which no amount of mod-side detection can
substitute for.
Tested on 2.0.0-alpha.2.