diff --git a/src/Metin2Launcher/Runtime/EnvVarKeyDelivery.cs b/src/Metin2Launcher/Runtime/EnvVarKeyDelivery.cs
new file mode 100644
index 0000000..4034de7
--- /dev/null
+++ b/src/Metin2Launcher/Runtime/EnvVarKeyDelivery.cs
@@ -0,0 +1,30 @@
+using System.Diagnostics;
+
+namespace Metin2Launcher.Runtime;
+
+///
+/// Delivers the runtime key to the game process via environment variables.
+/// This is the MVP mechanism — every platform supports it, no native interop,
+/// no timing windows. The variables are scoped to the child's environment only
+/// (we mutate , not the launcher's
+/// global process environment), so other processes on the machine never see them.
+///
+public sealed class EnvVarKeyDelivery : IRuntimeKeyDelivery
+{
+ public const string MasterKeyVar = "M2PACK_MASTER_KEY_HEX";
+ public const string SignPubkeyVar = "M2PACK_SIGN_PUBKEY_HEX";
+ public const string KeyIdVar = "M2PACK_KEY_ID";
+
+ public string Name => "env-var";
+
+ public void Apply(ProcessStartInfo psi, RuntimeKey key)
+ {
+ if (psi is null) throw new ArgumentNullException(nameof(psi));
+ if (key is null) throw new ArgumentNullException(nameof(key));
+
+ // Never set these on the launcher process itself — scope them to the child only.
+ psi.Environment[MasterKeyVar] = key.MasterKeyHex;
+ psi.Environment[SignPubkeyVar] = key.SignPubkeyHex;
+ psi.Environment[KeyIdVar] = key.KeyId;
+ }
+}
diff --git a/src/Metin2Launcher/Runtime/IRuntimeKeyDelivery.cs b/src/Metin2Launcher/Runtime/IRuntimeKeyDelivery.cs
new file mode 100644
index 0000000..bac6681
--- /dev/null
+++ b/src/Metin2Launcher/Runtime/IRuntimeKeyDelivery.cs
@@ -0,0 +1,18 @@
+using System.Diagnostics;
+
+namespace Metin2Launcher.Runtime;
+
+///
+/// Strategy for handing a to the game process. The
+/// MVP implements a single strategy () — the
+/// Windows shared-memory strategy is stubbed in SharedMemoryKeyDelivery
+/// and will be wired once the client-side receiver lands.
+///
+public interface IRuntimeKeyDelivery
+{
+ /// Name of the delivery mechanism, used for logs and tests.
+ string Name { get; }
+
+ /// Mutates so the spawned process can pick up the key.
+ void Apply(ProcessStartInfo psi, RuntimeKey key);
+}
diff --git a/src/Metin2Launcher/Runtime/RuntimeKey.cs b/src/Metin2Launcher/Runtime/RuntimeKey.cs
new file mode 100644
index 0000000..25a214d
--- /dev/null
+++ b/src/Metin2Launcher/Runtime/RuntimeKey.cs
@@ -0,0 +1,76 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Metin2Launcher.Runtime;
+
+///
+/// In-memory representation of a runtime key bundle delivered alongside an
+/// m2pack release. The launcher never decrypts or opens .m2p archives itself;
+/// it merely parses the key bundle produced by the release tool and passes it
+/// through to the game executable via one of the
+/// mechanisms.
+///
+/// File layout (runtime-key.json, placed next to the manifest by the release
+/// tool, never signed independently — its integrity comes from the signed
+/// manifest that references it):
+///
+/// {
+/// "key_id": "2026.04.14-1",
+/// "master_key_hex": "<64 hex chars>",
+/// "sign_pubkey_hex": "<64 hex chars>"
+/// }
+///
+///
+public sealed class RuntimeKey
+{
+ [JsonPropertyName("key_id")]
+ public string KeyId { get; set; } = "";
+
+ [JsonPropertyName("master_key_hex")]
+ public string MasterKeyHex { get; set; } = "";
+
+ [JsonPropertyName("sign_pubkey_hex")]
+ public string SignPubkeyHex { get; set; } = "";
+
+ private static readonly JsonSerializerOptions _opts = new()
+ {
+ PropertyNameCaseInsensitive = false,
+ };
+
+ /// Parses a runtime-key.json blob. Throws on malformed or incomplete data.
+ public static RuntimeKey Parse(ReadOnlySpan raw)
+ {
+ var rk = JsonSerializer.Deserialize(raw, _opts)
+ ?? throw new InvalidDataException("runtime-key deserialized to null");
+ Validate(rk);
+ return rk;
+ }
+
+ public static RuntimeKey Load(string path)
+ {
+ var bytes = File.ReadAllBytes(path);
+ return Parse(bytes);
+ }
+
+ private static void Validate(RuntimeKey rk)
+ {
+ if (string.IsNullOrWhiteSpace(rk.KeyId))
+ throw new InvalidDataException("runtime-key missing 'key_id'");
+ if (!IsHex(rk.MasterKeyHex, 64))
+ throw new InvalidDataException("runtime-key 'master_key_hex' must be 64 hex chars");
+ if (!IsHex(rk.SignPubkeyHex, 64))
+ throw new InvalidDataException("runtime-key 'sign_pubkey_hex' must be 64 hex chars");
+ }
+
+ private static bool IsHex(string s, int expectedLen)
+ {
+ if (s is null || s.Length != expectedLen) return false;
+ for (int i = 0; i < s.Length; i++)
+ {
+ char c = s[i];
+ bool ok = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
+ if (!ok) return false;
+ }
+ return true;
+ }
+}
diff --git a/src/Metin2Launcher/Runtime/SharedMemoryKeyDelivery.cs b/src/Metin2Launcher/Runtime/SharedMemoryKeyDelivery.cs
new file mode 100644
index 0000000..d4726dd
--- /dev/null
+++ b/src/Metin2Launcher/Runtime/SharedMemoryKeyDelivery.cs
@@ -0,0 +1,39 @@
+using System.Diagnostics;
+
+namespace Metin2Launcher.Runtime;
+
+///
+/// STUB. Windows shared-memory delivery for the runtime key is not implemented
+/// yet — the receiver on the client side has not landed. This type exists so
+/// the factory wiring in and the docs have a concrete
+/// name to reference, and so a future change can fill in the body without
+/// churning callers.
+///
+/// Planned implementation:
+/// 1. Create a named, write-only MemoryMappedFile with a random suffix
+/// (e.g. m2pack-key-<guid>) and a NamedPipeServerStream-style
+/// ACL that only grants access to the child PID once it has been spawned.
+/// 2. Write a short binary header (magic, key_id length, key_id, master_key,
+/// sign_pubkey) into the mapping.
+/// 3. Set an env var on the child with the mapping name so the receiver can
+/// OpenExisting and read the key before zeroing the source.
+/// 4. Zero and close the mapping from the launcher side once the child
+/// signals it has consumed the key (via ClientAppliedReporter).
+///
+/// Until then, calling throws — the factory must never
+/// select this delivery on a non-Windows host, and on Windows the code path
+/// is guarded behind a feature flag that is off by default.
+///
+public sealed class SharedMemoryKeyDelivery : IRuntimeKeyDelivery
+{
+ public string Name => "shared-memory";
+
+ public void Apply(ProcessStartInfo psi, RuntimeKey key)
+ {
+ // TODO(m2pack): implement Windows shared-memory delivery once the
+ // client-side receiver is in place. Tracked in docs/m2pack-integration.md.
+ throw new NotSupportedException(
+ "shared-memory runtime key delivery is not implemented yet; " +
+ "use EnvVarKeyDelivery for the MVP.");
+ }
+}