Every field in nebula.config.json — types, defaults, and what it does.
Nebula is driven by a JSON config file (nebula.config.json) passed with --config. This is the
complete list of fields. All are optional except inputs; unknown keys produce a warning rather
than being silently ignored.
{
"inputs": ["bin/Release/net8.0/MyApp.dll"],
"outputDirectory": "bin/Release/net8.0/obf",
"preservePublicApi": true,
"controlFlowObfuscation": true,
"encryptStrings": true
}
Editions. The Free edition does identifier renaming, up to 2 encrypted strings and up to
2 control-flow methods. Everything else (unlimited strings/control-flow, method & resource
encryption, reference (proxy-call) obfuscation, anti-tamper, anti-debug, environment/tampering-tool
detection, expiry guard, auto-protect licensing, metadata hardening, constant masking, aggressive
naming, cross-assembly rename, signing, PDB output, plugins) is Pro; aggressive
control-flow intensity, code virtualization and watermarking are Enterprise. See
Editions.
| Key | Type | Default | Description |
|---|
inputs | string[] | — (required) | Assembly path(s) to obfuscate. List all interdependent assemblies together for cross-consistent renaming — see Multiple assemblies. |
outputDirectory | string | <inputDir>/nebulated | Where obfuscated assemblies, symbol maps and reports are written. |
allowInPlace | bool | false | Safety guard: writing over the input assembly is refused unless this is true, so a mistyped path can’t destroy your source assembly. |
copyRuntimeConfig | bool | true | Copy each input’s *.runtimeconfig.json / *.deps.json next to the output so a framework-dependent app runs from the output folder. |
verifyOutput | bool | true | Re-check the transformed module for dangling references before writing and refuse to emit output that wouldn’t load. Leave on. |
schemaVersion | int | 1 | Config schema version, so older files get a clear upgrade warning. |
Renaming
| Key | Type | Default | Description |
|---|
preservePublicApi | bool | false | Keep public/protected type & member names; rename only internal/private. Turn on for a library referenced by code outside the run; off to rename everything eligible. |
autoDetect | bool | true | Automatically preserve names required for correctness — reflection/serialization/DI/XAML targets and [Obfuscation]-marked members. Leave on. |
include | string[] | [] | Allowlist of type full names eligible for renaming. When non-empty, only these types (and their members) are renamed; everything else is preserved. |
exclude | string[] | [] | Force-preserve names. A type full name preserves the type and all members; "Namespace.Type.Member" preserves one member. |
excludeNamespaces | string[] | [] | Force-preserve whole namespaces: any type whose namespace equals or starts with an entry (e.g. MyApp.Api) keeps its name and its members’. |
renameResources | bool | false | Rename embedded resource names and update references (incl. type-associated .resources so ResourceManager still resolves). |
aggressiveNaming | bool | false | Use hard-to-read Unicode-confusable identifiers instead of short ASCII names. Still deterministic and collision-free. |
crossAssemblyRename | bool | false | In a multi-assembly run, rename types/members shared across the input assemblies and rewrite every cross-assembly reference. Requires all referencing assemblies to be inputs — see Multiple assemblies. |
String encryption
| Key | Type | Default | Description |
|---|
encryptStrings | bool | false | Encrypt string literals in IL and decrypt at runtime with a per-build key. |
encryptStringsInclude | string[] | [] | Allowlist of exact literals to encrypt. When non-empty, only these are encrypted. Discover them with nebula inspect --strings. |
excludeStrings | string[] | [] | Exact literals to leave un-encrypted (e.g. license text). |
Control flow & code protection
| Key | Type | Default | Description |
|---|
controlFlowObfuscation | bool | false | Flatten method control flow into a dispatcher state machine. Works for all method shapes, including try/catch/finally (the top level and each try body are flattened; nested handlers are kept atomic). Anything not provably safe to move is left intact. |
controlFlowIntensity | string | "normal" | none / light / normal / aggressive — roughly 0 / 25 / 60 / 100 % of eligible methods when no explicit include list is given. aggressive is Enterprise-only; on a lower tier it downgrades to normal (and with requireLicensedEdition that downgrade fails the build). |
controlFlowInclude | string[] | [] | Allowlist of methods to flatten, by Namespace.Type.Method. When non-empty, only these are transformed. Discover names with nebula inspect --methods. |
controlFlowExclude | string[] | [] | Methods to skip, by Namespace.Type.Method. |
obfuscateConstants | bool | false | Mask integer literal loads with a per-build XOR key resolved at load time (decoded inline at runtime). This is lightweight masking, not cryptography — it breaks the plain literal and defeats naive constant scanning. |
proxyReferences | bool | false | Paid. Route call/callvirt through injected, unbranded proxy methods so a decompiler / de4dot can’t read which external or BCL method a site calls. Conservative and behaviour-preserving (skips constructors, generics, value-type instance calls, etc.). |
virtualizeMethods | bool | false | Enterprise. Compile eligible methods to a custom bytecode run by an embedded VM — the logic no longer exists as IL. See code virtualization. |
virtualizeInclude | string[] | [] | Allowlist of methods to virtualize by Namespace.Type.Method (empty = all eligible). |
encryptMethods | bool | false | Strongest static-code protection: encrypt a method’s whole IL and re-emit it at call time via the Nebula.Runtime helper (copied next to the output), keeping the IL encrypted at rest in memory (anti-dump). Eligibility is deliberately narrow — it covers static, non-generic methods with no exception handlers and no by-ref/pointer parameters; anything else is left untouched. Note the trade-offs: once the CLR JIT-compiles a decrypted method its native code still resides in memory, and decrypting on each call adds per-invocation overhead, so target it at a few high-value methods rather than hot paths. |
encryptResources | bool | false | Encrypt embedded resources read via Assembly.GetManifestResourceStream(string) and decrypt on demand. (.resources for ResourceManager/WinForms/satellite are skipped.) |
Anti-tamper & anti-debug
| Key | Type | Default | Description |
|---|
antiTamper | bool | false | Inject a load-time integrity check (SHA-256 of the written image) that reacts if the file was modified after obfuscation. Stops casual patching. |
antiTamperReaction | string | "terminate" | On mismatch: terminate (FailFast), throw, exit, or callback. |
antiTamperCallback | string | null | Namespace.Type.Method of a static, parameterless method to call when reaction is callback (must be preserved so it resolves). |
antiDebug | bool | false | Inject a managed-debugger detection check that runs on module load. Off by default so it never hinders your own QA builds. |
antiDebugReaction | string | "throw" | On a detected debugger: throw, terminate, exit, or callback. |
antiDebugCallback | string | null | Callback method for the anti-debug callback reaction. |
environmentChecks | bool | false | Inject a load-time check that reacts when running under a CLR profiler (CORECLR_ENABLE_PROFILING / COR_ENABLE_PROFILING) or a known runtime-patching framework — Harmony or MonoMod.RuntimeDetour (Detour/Hook/NativeDetour). Detection is by loaded managed type; a purely native inline hook or raw DLL injection is out of scope. Off by default so it never hinders your own profiling/QA. |
environmentChecksReaction | string | "throw" | On detection: throw, terminate, exit, or callback. |
environmentChecksCallback | string | null | Callback method for the environment-check callback reaction. |
environmentChecksContinuous | bool | false | Keep watching beyond load time (requires environmentChecks): subscribe to AppDomain.AssemblyLoad so a patcher attaching after startup is caught when it loads, and re-run the check at the entry of the methods that call your licensing SDK (the same ones autoProtectLicensing finds, via licensingNamespaces) so a late-attached debugger/patcher is caught at the gate. Reuses the same reaction. |
environmentChecksIntervalSeconds | int | 0 | With continuous mode, also re-check on a background timer every N seconds (0 = no timer; the AssemblyLoad hook and call-site re-checks still apply). The first fire is jittered per build so the cadence isn’t trivially identifiable. |
autoProtectLicensing | bool | false | Auto-target the methods that call your licensing SDK for the strongest protection the edition applies: force-include them in virtualization (if enabled) and control-flow flattening, and never exclude them — no need to name the gate methods. Paid feature. |
licensingNamespaces | string[] | ["Keyright"] | Root namespaces autoProtectLicensing treats as the licensing SDK. Defaults to Keyright; add any other licensing SDK’s root namespace. |
expiryUtc | date | null | Expiry / “time-bomb”: a UTC instant after which the protected build stops working (for betas, evaluations, embargoed releases). A load-time guard reacts once the date passes. Checked against the machine clock, so pair with antiTamper. |
expiryReaction | string | "throw" | On expiry: throw, terminate, exit, or callback. |
expiryCallback | string | null | Callback method for the expiry callback reaction. |
tamperReportUrl | string | null | Built-in tamper telemetry (“report-home”): when set, a guard that fires (anti-tamper, anti-debug, environment checks, expiry) also POSTs a minimal {"event":"…"} to this URL — your own endpoint (never Delta1 Labs) before its reaction, so you get telemetry without writing a callback. The send is bounded and self-swallowing — it can’t block or crash the app. Only the event name is sent (no source, assembly contents or personal data). Entirely opt-in: leave it unset and nothing is ever sent. It is your telemetry to your endpoint, so it is independent of NEBULA_NO_TELEMETRY — that variable only silences Nebula’s own anonymous tool analytics (a separate build-time channel) and has no effect on report-home. Paid feature; adds outbound traffic from protected apps (not for air-gapped). |
expiryMessage | string | null | Optional custom message surfaced by the expiry guard. |
antiDebug, environmentChecks and expiryUtc are paid-edition features (dropped in the Free edition). antiDebug and expiryUtc are load-time guards; environmentChecks is load-time by default but can run continuously (environmentChecksContinuous) to also catch a patcher/debugger attached after startup. All of them deter tampering rather than defeating a fully determined attacker, so combine them with antiTamper and control-flow/string protection.
| Key | Type | Default | Description |
|---|
metadataHardening | bool | false | Strip compile-time-only metadata decompilers use but the CLR doesn’t need (nullable annotation attributes, etc.), preserving runtime-critical metadata. |
pdbMode | string | "none" | none discards debug symbols (max protection); obfuscated emits a PDB matching the obfuscated IL for your private crash analysis. |
Signing
| Key | Type | Default | Description |
|---|
strongNameKeyFile | string | null | Path to a .snk to re-sign the output (renaming invalidates any existing strong name). Relative paths resolve against the config file. |
strongNameKeyEnvVar | string | null | Name of an env var holding a base64-encoded .snk, so keys need not live in the config. Takes precedence over strongNameKeyFile. |
delaySign | bool | false | Delay-sign only (embed the public key, leave room for a later signature). |
authenticodeCertFile | string | null | .pfx to Authenticode-sign the output (after strong-name signing). Requires signtool / Windows SDK. |
authenticodeCertPassword | string | null | Password for the .pfx — prefer supplying via an env var over inline text. |
authenticodeCertThumbprint | string | null | SHA-1 thumbprint of a cert in the Windows store to sign with (alternative to a .pfx). |
authenticodeTimestampUrl | string | null | Optional RFC-3161 timestamp URL for Authenticode signing. |
Licensing, incremental & extensibility
| Key | Type | Default | Description |
|---|
licenseFile | string | null | Path to a license file. When omitted, the NEBULA_LICENSE env var and default locations are checked. Without a valid license, the Free edition runs. |
requireLicensedEdition | bool | false | Fail the run if the resolved license can’t apply the requested protection — it resolved to Free (missing / revoked / lapsed license), or a Pro license had an Enterprise-only feature (e.g. aggressive control flow, watermarking) downgraded. Stops a build server from silently shipping weakly-obfuscated output; recommended for CI. See licensing. |
seedMapFile | string | null | A previous run’s .symbols.json for incremental/seeded obfuscation: unchanged members reuse their previous obfuscated names; only added/changed members get new names. |
plugins | string[] | [] | Paths to plugin assemblies providing extra IObfuscationPass implementations. Each is appended to the pipeline. |
watermark | string | null | Enterprise. A per-customer identifier embedded (obfuscated) into the output so a leaked binary traces back to the customer. Read it with nebula watermark --read. |
A fuller example
{
"inputs": ["out/MyApp.dll", "out/MyApp.Core.dll"],
"outputDirectory": "out/obf",
"preservePublicApi": false,
"crossAssemblyRename": true,
"encryptStrings": true,
"controlFlowObfuscation": true,
"controlFlowIntensity": "normal",
"obfuscateConstants": true,
"antiTamper": true,
"metadataHardening": true,
"strongNameKeyEnvVar": "MYAPP_SNK_BASE64",
"pdbMode": "none"
}
See also: CLI reference · MSBuild & CI ·
Multiple assemblies.