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.

powershell
# 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

powershell
> 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

powershell
# 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.

FlagEffect
--dry-runPerform every read — so permission problems still surface — but skip all writes.
-y, --yesSkip confirmation prompts.
--output text|jsonMachine-readable output for pipelines.
--view 64|32|bothChoose the WOW64 registry view explicitly instead of inheriting process bitness.
--log-levelerror, warn, info (default) or debug.
--self-checkReport 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.

CodeMeaningTypical cause
0Success
2Usage errorUnknown flag, missing argument.
3Parse errorThe .reg file has invalid syntax.
4Access deniedThe key exists but this token cannot write to it.
5Partial successSome keys were skipped or some subkeys were unreadable.
6Redirection refusedA key had no per-user equivalent and --on-refuse fail was set.
7File I/O errorCould not read or write a file on disk.
8Not foundThe key or value does not exist.

validate

Parse and lint one or more .reg files. Touches the registry only if you ask it to.

usage
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.

DefectRepairClass
hex(1)/hex(2)/hex(6) missing NUL terminatorAppend 00,00safe
hex(7) missing double NULAppend until the list terminatessafe
Trailing whitespace after a \ continuationRemoved — regedit stops folding there and drops the rest of the payloadsafe
Control characters in a key path or value nameStrippedsafe
Duplicate key blocksCoalesced, last write winssafe / lossy
Odd-length UTF-16 payloadPadded with one NUL bytelossy
hex(4)/hex(b) of the wrong lengthReported, never guessednot 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.

usage
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.

usage
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.

usage
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.

usage
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

usage
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.

powershell
> 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:

does not 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.

powershell
# 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

OperationDescription
infoSize, regf signature, and whether the hive can be mounted read-only or read/write. Run this first.
lsList subkeys.
queryPrint values under a subkey.
setWrite one value.
deleteDelete a subkey or a single value.
importMerge a .reg file in. --strip-root removes the mount-point prefix it was exported under.
exportWrite part of the hive to a .reg file. --root-as sets the root label, because the .reg format has no syntax for "app hive".
execRun 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.

powershell
> 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.

powershell
> 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

RankOriginNotes
1explicit pathPassing a config file rather than an executable anchors on its directory and records the file here.
2environment variable<STEM>_CONFIG, <STEM>_HOME, <STEM>_INI.
3beside the executableThe sidecar, plus the .exe.<ext> convention.
4–6%LOCALAPPDATA%, %APPDATA%, %PROGRAMDATA%Under a \<stem>\ subdirectory.
7registry pointerSoftware\<stem> ConfigPath. Opt in with --registry-pointer.
8Group Policy cachesRegistry.pol and PolicyDefinitions. Opt in with --policy.
9current directoryAlways probed so its risk can be reported. Never trusted.
10%WINDIR%Where GetPrivateProfileString silently resolves a bare file name.

Risks reported

RiskWhy it matters
CurrentDirectoryAnyone who can write to a directory a user launches from controls that configuration. This is config planting — the same shape as DLL planting.
UserWritableThe file sits somewhere this user can write, while the executable's own directory is protected. A lower-privileged location overriding a higher-privileged one.
ReparsePointReached through a symlink or junction; the real target may be elsewhere entirely.
EscapesAnchorA sidecar whose resolved path leaves the anchor directory got there through a link.
NetworkPathUNC or a mapped drive: availability and integrity are not local.
WindowsFallbackThe %WINDIR% resolution of the profile-string APIs, which plenty of legacy code hits without intending to.
ShortNameAliasMatched 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.

FormatTypical fileNotes
reg.regUTF-16 Version 5.00 or ANSI REGEDIT4.
polRegistry.polGroup Policy PReg binary. See below.
admx.admx + .admlPolicy template. Concrete values only; elements reported.
gppRegistry.xmlGroup Policy Preferences, actions C/R/U/D.
inf.inf[AddReg]/[DelReg] with [Strings] substitution.
json.jsonCompact or explicit form.
csv.csv, .tsvHeader-driven columns, delimiter auto-detected.
ini.ini, .cfgSection per key path.
hiveNTUSER.DATDetected, 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.

DirectiveHandled as
**del.Namedelete the value Name
**delvals.delete every value in the key — expressed as a key delete, which also removes subkeys, and reported as such
**DeleteValuesexpands the ;-separated list into individual value deletes
**DeleteKeysexpands into [-KEY] blocks for each named subkey
**soft.Name"write only if absent" — applied unconditionally, and reported, because .reg has no equivalent
**SecureKey, **ListElementno 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 own valueName
  • the documented ADMX default when neither is declared — enabling writes REG_DWORD 1, disabling writes REG_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.

acme.json
{
  "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:

explicit form
{ "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.

acme.csv
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.

acme.ini
[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.

FlagValuesMeaning
--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 formatExample
REG_SZLiteral text-d "C:\Tools"
REG_EXPAND_SZText with environment variables-d "%USERPROFILE%\bin"
REG_MULTI_SZEntries separated by a literal \0-d "a.txt\0b.txt"
REG_DWORDDecimal or 0x hex-d 25 or -d 0x19
REG_QWORDDecimal or 0x hex-d 0xdeadbeef
REG_BINARYHex 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.

  1. 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.

  2. Run from a path the policy already allows

    Typically %ProgramFiles% or an IT-managed share, rather than Downloads.

  3. Clear the Mark-of-the-Web

    Unblock-File removes the Zone.Identifier stream that triggers the SmartScreen interstitial.

signing
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 \n or \t — writing one produces a literal backslash followed by that character.
  • Two encodings. Windows Registry Editor Version 5.00 is UTF-16LE with a BOM; REGEDIT4 is ANSI in the machine's codepage. A REGEDIT4 file written on one codepage decodes differently on another.
  • Continuations are hex-only. A trailing \ continues a hex payload. A string value ending in "C:\\" is not a continuation.
  • Strings need terminators. hex(1), hex(2) and hex(7) payloads must be NUL-terminated — REG_MULTI_SZ doubly 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.