Documentation
regx is a single-file Windows Registry CLI for standard users. Every command below runs
without elevation. Where a command physically cannot succeed without admin rights, it says so and
exits with a specific code rather than failing obscurely.
Install
There is nothing to install. Download regx.exe, put it anywhere you can write, and run it.
It creates no registry entries, no service, no scheduled task and no %APPDATA% folder.
# Verify it runs and see what the environment allows
> .\regx.exe --self-check
regx self-check
[ok ] process 64-bit process. HKLM\SOFTWARE resolves to the 64-bit view.
[ok ] elevation not elevated (medium integrity), as intended
[ok ] hkcu HKCU\Software is writable - the redirection target is usable
A file downloaded with a browser carries a Mark-of-the-Web stream that triggers SmartScreen.
--self-check reports it; clear it with Unblock-File .\regx.exe.
Build from source
> cargo build --release # -> target\release\regx.exe
Requires a Rust MSVC toolchain. The build embeds the asInvoker manifest into the PE and
statically links the CRT, so the result needs no Visual C++ redistributable.
Quick start
# 1. Lint and repair a .reg file you found online
regx validate app.reg --fix --backup
# 2. See where each key would land, without writing anything
regx convert app.reg --redirect auto
# 3. Check you can actually write to the destination
regx probe "HKCU\Software\Acme"
# 4. Apply it. An undo file is written before anything changes.
regx import app.reg
# 5. Changed your mind
regx import app.undo.reg --no-backup
Global flags
These apply to every command.
| Flag | Effect |
|---|---|
--dry-run | Perform every read — so permission problems still surface — but skip all writes. |
-y, --yes | Skip confirmation prompts. |
--output text|json | Machine-readable output for pipelines. |
--view 64|32|both | Choose the WOW64 registry view explicitly instead of inheriting process bitness. |
--log-level | error, warn, info (default) or debug. |
--self-check | Report what AppLocker, SRP, WDAC and the process token do to this binary, then continue or exit. |
Exit codes
Stable across releases, so a script can branch on them.
| Code | Meaning | Typical cause |
|---|---|---|
0 | Success | — |
2 | Usage error | Unknown flag, missing argument. |
3 | Parse error | The .reg file has invalid syntax. |
4 | Access denied | The key exists but this token cannot write to it. |
5 | Partial success | Some keys were skipped or some subkeys were unreadable. |
6 | Redirection refused | A key had no per-user equivalent and --on-refuse fail was set. |
7 | File I/O error | Could not read or write a file on disk. |
8 | Not found | The key or value does not exist. |
validate
Parse and lint one or more .reg files. Touches the registry only if you ask it to.
regx validate <FILE...> [--strict] [--fix [-o FILE] [--backup]]
What --fix repairs
Safe repairs are unambiguously what the author meant. Lossy repairs change bytes — they are still applied, because the file is already broken, but they are labelled so you can judge.
| Defect | Repair | Class |
|---|---|---|
hex(1)/hex(2)/hex(6) missing NUL terminator | Append 00,00 | safe |
hex(7) missing double NUL | Append until the list terminates | safe |
Trailing whitespace after a \ continuation | Removed — regedit stops folding there and drops the rest of the payload | safe |
| Control characters in a key path or value name | Stripped | safe |
| Duplicate key blocks | Coalesced, last write wins | safe / lossy |
| Odd-length UTF-16 payload | Padded with one NUL byte | lossy |
hex(4)/hex(b) of the wrong length | Reported, never guessed | not fixed |
--fix refuses to run on a file with syntax errors. Repairing those would mean guessing the author's intent rather than fixing a known defect.
convert
Transform a .reg file offline. This command never touches the registry, so it is the safe way to preview a redirection.
regx convert <FILE> [-o FILE] [--redirect MODE] [--min-confidence LEVEL] [--reg4]
With no -o, the result is written to standard output. --reg4 emits the legacy ANSI
REGEDIT4 dialect instead of UTF-16 Version 5.00.
merge
Combine several .reg files into one. Duplicate keys are folded case-insensitively with last-write-wins, and every value conflict is reported.
regx merge <FILE> <FILE...> [-o FILE]
import and sync
Merge .reg files into the live registry. Both write an undo snapshot before making any change.
regx import <FILE...> [--redirect MODE] [--backup FILE | --no-backup]
regx sync <FILE> [--redirect MODE] [--prune]
The undo snapshot
The registry offers no transaction, so a failed merge would otherwise leave half-applied state.
Before writing, regx computes the inverse of the pending change and saves it as an ordinary
.reg file — <input>.undo.reg by default.
[-KEY]that exists → the whole subtree is exported, so undo recreates it- Key exists, value exists → the current data is recorded
- Key exists, value absent →
"name"=-is recorded - Key does not exist →
[-TOPMOST_MISSING_ANCESTOR], not[-KEY]— deleting only the leaf would leave the intermediate keys behind as empty shells
Restores are ordered before removals. If any key could not be read, the undo file is reported as INCOMPLETE rather than silently trusted.
sync --prune
Makes the apply idempotent: any live value under a declared key that the file does not mention becomes
an explicit delete. Use it when the .reg file is meant to be the complete desired state.
export
Write a key to a .reg file, byte-compatible with regedit's own output so diffs are meaningful.
regx export <KEY> [-o FILE] [-r] [--reg4]
A denied subkey never aborts the export. Partial export of your own hive is normal — Group-Policy-locked
policy keys and Protected subtrees are common — so skips are listed and the command exits
with code 5.
query, set, delete
regx query <KEY> [-v NAME] [-r]
regx set <KEY> -v NAME -t TYPE -d DATA [--redirect MODE]
regx delete <KEY> [-v NAME] [-r]
delete without -v removes the key and its subkeys, so it requires -r
as an explicit acknowledgement.
probe
Answers the only question that matters before an import: can this user actually write here?
It really opens the key rather than inferring from the path, because an ACL on a single subkey can deny
a standard user even inside their own HKCU.
> regx probe "HKLM\SOFTWARE\Microsoft"
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft
exists true
readable true
writable false
creatable false
probe has no side effects: when the key does not exist it walks up to the nearest existing
ancestor and tests write access there instead of creating a scratch key.
hive — offline hive files
RegLoadKey, which reg load and regedit's Load Hive both call, requires
SeRestorePrivilege. RegLoadAppKeyW does not: it mounts the hive into a private,
unnamed slot that only the calling process can see, so there is no global namespace entry to protect and
no privilege to check.
Why mount and unmount are not separate commands
The handle is process-scoped. Closing it — including at process exit — unloads the hive. So this cannot work:
regx hive mount NTUSER.DAT --as my_hive # process 1 exits -> hive unloaded
regx hive set my_hive\Software\App ... # process 2: nothing is mounted
regx hive unmount my_hive # process 3: nothing to unmount
There is no supported workaround. The handle cannot be published to the registry namespace — that is exactly what the privilege check guards — and inheriting it into another process would require the mounting process to stay alive, i.e. a resident daemon, which defeats "portable, no install". Mount, operate and unmount therefore happen inside one process.
# Several operations under a single mount
regx hive "C:\path\MyApp.hive" --create exec \
-c "set Software\MyApp -v License -d OK" \
-c "set Software\MyApp -v Seats -t REG_DWORD -d 25" \
-c "query Software\MyApp -r"
# A single operation needs no exec
regx hive "C:\path\MyApp.hive" export Software -o offline.reg --root-as "HKEY_USERS\OFFLINE"
Operations
| Operation | Description |
|---|---|
info | Size, regf signature, and whether the hive can be mounted read-only or read/write. Run this first. |
ls | List subkeys. |
query | Print values under a subkey. |
set | Write one value. |
delete | Delete a subkey or a single value. |
import | Merge a .reg file in. --strip-root removes the mount-point prefix it was exported under. |
export | Write part of the hive to a .reg file. --root-as sets the root label, because the .reg format has no syntax for "app hive". |
exec | Run several operations under one mount, via repeated -c or a --script file. |
What you can realistically open
Write access to the file is still required, and a hive already mounted by the OS is held
exclusively. Realistic targets: a logged-off secondary profile, a copy of a hive, an
application's private hive, or a hive on a mounted backup or VHD. A logged-on user's
NTUSER.DAT fails with ERROR_SHARING_VIOLATION by design —
hive info reports this before you commit to a write.
inspect
Report what a file is and what it contains, without applying any of it. Use it on anything you did not write yourself before deciding whether to import.
> regx inspect "C:\Windows\System32\GroupPolicy\Machine\Registry.pol"
format pol
key blocks 2 (0 whole-key delete(s))
values 6
hives HKEY_LOCAL_MACHINE
note 5 policy record(s)
note policy paths rooted at HKEY_LOCAL_MACHINE (a .pol stores no hive)
1 key(s) have no per-user equivalent; `regx convert` shows which
regx formats lists every supported format and how each is detected.
discover
Enterprise executables find their own configuration by anchoring on
GetModuleFileNameW(NULL) — the real path of the running module. It is used
rather than argv[0] because a parent process chooses argv[0] and
can point it anywhere. Strip the extension, append .ini, and that is the classic
sidecar; .NET reaches MyApp.exe.config the same way. Around that, products layer
a search order.
discover reproduces that search, reports which rung each hit came from, and
flags the rungs that are load-bearing security bugs.
> regx discover "C:\Program Files\Acme\updater.exe" --strict
anchor C:\Program Files\Acme
stem updater
4 companion file(s), in search order:
[3] beside the executable C:\Program Files\Acme\updater.ini
ini 84 bytes
[9] current directory D:\Shared\updater.ini
ini 74 bytes
RISK CurrentDirectory: anyone who can write there controls this configuration
The search order
| Rank | Origin | Notes |
|---|---|---|
| 1 | explicit path | Passing a config file rather than an executable anchors on its directory and records the file here. |
| 2 | environment variable | <STEM>_CONFIG, <STEM>_HOME, <STEM>_INI. |
| 3 | beside the executable | The sidecar, plus the .exe.<ext> convention. |
| 4–6 | %LOCALAPPDATA%, %APPDATA%, %PROGRAMDATA% | Under a \<stem>\ subdirectory. |
| 7 | registry pointer | Software\<stem> ConfigPath. Opt in with --registry-pointer. |
| 8 | Group Policy caches | Registry.pol and PolicyDefinitions. Opt in with --policy. |
| 9 | current directory | Always probed so its risk can be reported. Never trusted. |
| 10 | %WINDIR% | Where GetPrivateProfileString silently resolves a bare file name. |
Risks reported
| Risk | Why it matters |
|---|---|
CurrentDirectory | Anyone who can write to a directory a user launches from controls that configuration. This is config planting — the same shape as DLL planting. |
UserWritable | The file sits somewhere this user can write, while the executable's own directory is protected. A lower-privileged location overriding a higher-privileged one. |
ReparsePoint | Reached through a symlink or junction; the real target may be elsewhere entirely. |
EscapesAnchor | A sidecar whose resolved path leaves the anchor directory got there through a link. |
NetworkPath | UNC or a mapped drive: availability and integrity are not local. |
WindowsFallback | The %WINDIR% resolution of the profile-string APIs, which plenty of legacy code hits without intending to. |
ShortNameAlias | Matched only after 8.3 expansion, so path comparisons elsewhere in the system may not agree that these are the same file. |
Directory writability is asked of the OS, not inferred from the path: the
directory is opened for FILE_ADD_FILE, which is an access check with no side
effect — the same principle as probe. And
discover reports what an application would find; it does not claim to
know which rungs a given product actually implements. Confirm before trusting a hit.
--strict exits 5 if any hit carries a risk, so a deployment check can gate on it.
Input formats
import, convert, sync and inspect all
accept any format below. Each reader produces the same internal model, so redirection,
coalescing, undo snapshots and apply behave identically whatever the input was.
regx formats prints this table.
| Format | Typical file | Notes |
|---|---|---|
reg | .reg | UTF-16 Version 5.00 or ANSI REGEDIT4. |
pol | Registry.pol | Group Policy PReg binary. See below. |
admx | .admx + .adml | Policy template. Concrete values only; elements reported. |
gpp | Registry.xml | Group Policy Preferences, actions C/R/U/D. |
inf | .inf | [AddReg]/[DelReg] with [Strings] substitution. |
json | .json | Compact or explicit form. |
csv | .csv, .tsv | Header-driven columns, delimiter auto-detected. |
ini | .ini, .cfg | Section per key path. |
hive | NTUSER.DAT | Detected, then redirected to regx hive. |
Detection reads content first and the extension second. Override it with
--from <FORMAT> when a file is mislabelled.
Registry.pol
The binary a domain controller pushes down and the Group Policy engine replays. The cached
copies under %WINDIR%\System32\GroupPolicy and
…\GroupPolicyUsers are readable by ordinary users, so you can see exactly which
registry writes a policy performs — no elevation, no guessing.
| Directive | Handled as |
|---|---|
**del.Name | delete the value Name |
**delvals. | delete every value in the key — expressed as a key delete, which also removes subkeys, and reported as such |
**DeleteValues | expands the ;-separated list into individual value deletes |
**DeleteKeys | expands into [-KEY] blocks for each named subkey |
**soft.Name | "write only if absent" — applied unconditionally, and reported, because .reg has no equivalent |
**SecureKey, **ListElement | no registry effect; ignored and reported |
A .pol records no hive. The same bytes mean HKLM under
Machine\ and HKCU under User\, so the root has to come from
outside the file. regx infers it from the path and falls back to
--pol-root; the choice is always printed.
ADMX and ADML
An ADMX is a schema, not data: it declares which registry values a policy controls, not what an administrator chose. That distinction drives what is emitted.
Emitted, because it is concrete:
<enabledValue>/<disabledValue>on the policy's ownvalueName- the documented ADMX default when neither is declared — enabling writes
REG_DWORD 1, disabling writesREG_DWORD 0 <enabledList>/<disabledList>items, which carry literal values and may override the key
Reported, never emitted: <elements> — text,
decimal, boolean, enum, list,
multiText. Those hold whatever an administrator typed into the Group Policy
editor. Inventing a value for them would put fabricated data into the registry, so
regx lists the value names a policy owns and stops there. To see what was
actually configured, read the Registry.pol instead.
class picks the hive: Machine → HKLM, User → HKCU,
Both → emitted into both, which is what Windows does. An accompanying
.adml in a language folder is found automatically and resolves the
$(string.Id) display names. Choose the rendered state with
--admx-state enabled|disabled and narrow to one policy with
--admx-policy.
Group Policy Preferences
The other half of Group Policy. Where an ADMX declares a schema and a
Registry.pol carries the policy branch, GPP writes anywhere in the registry —
and its writes are not reverted when the GPO stops applying unless the item
says so. That is frequently why a setting keeps coming back.
Actions Create, Replace and Update all end in a
written value; only D deletes, and a D with an empty
name deletes the whole key. <Collection> groups are
traversed, REG_MULTI_SZ entries are read from the
<Values> children rather than the value attribute, and items
marked disabled="1" are skipped and counted.
Both XML readers refuse a DOCTYPE rather than skipping it.
A DOCTYPE is where external-entity and billion-laughs attacks live, and neither format
needs one — an XML parser that resolves external entities in a tool reading files from a
policy share is an XXE vulnerability waiting to happen. Nesting depth is bounded too.
INF
Reads every [AddReg] and [DelReg] section named by an
AddReg=/DelReg= directive; restrict it with
--inf-section. %Token% references resolve against
[Strings], and %% stays a literal percent. Unknown tokens are left
verbatim rather than becoming an empty string the author never intended.
HKR is skipped and reported — it is relative to a driver install context that
only exists inside SetupAPI, so there is no honest path to map it to.
JSON
The compact form is what you write by hand. Types map on sight.
{
"HKCU\\Software\\Acme": {
"Server": "acme.test", // REG_SZ
"Port": 8080, // REG_DWORD
"Enabled": true, // REG_DWORD 1
"Recent": ["a.txt", "b.txt"], // REG_MULTI_SZ
"Blob": { "type": "REG_BINARY", "data": "de ad be ef" },
"Legacy": null // delete the value
}
}
An integer too wide for a DWORD widens to REG_QWORD rather than truncating.
Floating-point numbers are refused: the registry has no such type, and silently rounding
would be worse than an error. Registry paths need doubled backslashes —
"HKCU\\Software", not "HKCU\Software" — because a lone backslash
is not a valid JSON escape.
The explicit form adds key deletion and named types:
{ "keys": [
{ "path": "HKCU\\Software\\Gone", "delete": true },
{ "path": "HKCU\\Software\\Acme",
"values": [ { "name": "Port", "type": "REG_DWORD", "data": 8080 } ] }
] }
CSV
Columns are matched by header name in any order, case-insensitively, so a sheet exported
from Excel works unedited. Quoting follows RFC 4180. An empty type and data deletes
the value; DELETE_KEY in the data column with no value name deletes the key.
key,name,type,data
HKCU\Software\Acme,Server,REG_SZ,acme.test
HKCU\Software\Acme,Port,REG_DWORD,8080
HKCU\Software\Acme,Legacy,, # delete the value
HKCU\Software\Old,,,DELETE_KEY # delete the key
INI
A section header is a full registry path and [-Path] deletes the key, both
borrowed from .reg. The optional :type suffix is the only addition;
without it a value is REG_SZ, which is what an ordinary INI means anyway.
REG_MULTI_SZ entries are separated with |, which reads better here
than reg.exe's \0.
[HKEY_CURRENT_USER\Software\Acme]
Server = acme.test
Port:dword = 8080
Path:expand_sz = %USERPROFILE%\acme
Recent:multi_sz = a.txt|b.txt
Blob:binary = 01 02 ff
Legacy = ; empty value deletes it
@ = default value
[-HKEY_CURRENT_USER\Software\Old]
Redirection flags
Shared by import, convert, sync and set.
| Flag | Values | Meaning |
|---|---|---|
--redirect |
off, auto, classes-only, force |
Default auto. classes-only maps just Software\Classes, the one mapping reliable by design. force implies the lowest confidence floor. |
--min-confidence |
high, medium, low |
Default medium. Mappings weaker than this are skipped and reported. |
--on-refuse |
skip, fail |
What to do with keys that have no per-user equivalent at all. Default skip. |
SOFTWARE\WOW6432Node\X is normalised to SOFTWARE\X before classification, so
32-bit and 64-bit exports of the same application collapse onto one destination. Because that collapse
produces duplicate key blocks, every redirect run is followed by a case-insensitive coalesce pass.
Value types
set follows reg.exe conventions, so muscle memory transfers.
| Type | -d format | Example |
|---|---|---|
REG_SZ | Literal text | -d "C:\Tools" |
REG_EXPAND_SZ | Text with environment variables | -d "%USERPROFILE%\bin" |
REG_MULTI_SZ | Entries separated by a literal \0 | -d "a.txt\0b.txt" |
REG_DWORD | Decimal or 0x hex | -d 25 or -d 0x19 |
REG_QWORD | Decimal or 0x hex | -d 0xdeadbeef |
REG_BINARY | Hex digits, separators optional | -d "01 02 03" |
Getting past AppLocker, SRP and WDAC
UAC is not the real obstacle in a locked-down enterprise; application control is. An unsigned
.exe under %TEMP%, Downloads or %APPDATA% is precisely
the shape the default rule sets deny. regx --self-check reads the relevant policy keys —
all readable by a standard user — and reports what applies.
-
Sign the binary
A publisher rule follows the file anywhere; a path rule does not. An EV certificate additionally carries immediate SmartScreen reputation, which a standard OV certificate has to accumulate. In a managed environment a certificate from the organisation's internal CA is usually faster to obtain and already trusted domain-wide.
-
Run from a path the policy already allows
Typically
%ProgramFiles%or an IT-managed share, rather thanDownloads. -
Clear the Mark-of-the-Web
Unblock-Fileremoves theZone.Identifierstream that triggers the SmartScreen interstitial.
signtool sign /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 /a regx.exe
Always timestamp with /tr — without it, signatures stop validating when the certificate expires.
WDAC is different. It ignores file location entirely. If a user-mode code-integrity policy is deployed, only a signature or an explicit hash rule will let the binary run — steps 2 and 3 do not help.
.reg format notes
Behaviours that surprise people writing .reg files by hand, all of which regx matches to regedit:
- Two escapes only.
\\and\". There is no\nor\t— writing one produces a literal backslash followed by that character. - Two encodings.
Windows Registry Editor Version 5.00is UTF-16LE with a BOM;REGEDIT4is ANSI in the machine's codepage. A REGEDIT4 file written on one codepage decodes differently on another. - Continuations are hex-only. A trailing
\continues ahexpayload. A string value ending in"C:\\"is not a continuation. - Strings need terminators.
hex(1),hex(2)andhex(7)payloads must be NUL-terminated —REG_MULTI_SZdoubly so. Without them the consuming application reads past the value. - Comments are line-initial. A
;starts a comment only at the beginning of a line, and its behaviour inside a hex continuation is inconsistent in regedit itself.
regx preserves values it cannot model losslessly as raw hex(N) bytes, and only
writes a REG_SZ as a quoted string when it is clean UTF-16 — even length, single trailing NUL,
no embedded NUL, no control characters. A raw newline inside a quoted string would corrupt the next line
of the file.