MoreRSS

site iconDen DelimarskyModify

I am an engineer and product manager, currently working at Microsoft in the security organization helping the team ship secure and performant authentication and authorization libraries.
Please copy the RSS to your reader, or quickly subscribe to:

Inoreader Feedly Follow Feedbin Local Reader

Rss preview of Blog of Den Delimarsky

Halo: Campaign Evolved's Secret Difficulty Modifiers

2026-07-26 08:00:00

After I got the debug menu up and running, I went looking for the next thing the game wasn’t telling me about out of the box - because, of course I would.

It didn’t take too long, since I am writing this post so quickly after the previous one. As it turns out, there’s a bunch of customizations that are only partially exposed through the in-game UI, that you can set through some configuration *.ini files.

Halo: Campaign Evolved has a Difficulty Modifiers screen when you start a campaign mission. It gives you a handful of dropdowns where you can customize damage resistance, shield regeneration, player damage output, melee damage, and ammo, among others. This is actually a really neat feature that allows the player to adjust the game to the level of difficulty they feel like playing under, and I wish more games had this.

While the options in the UI are extensive as-is, digging through the binary and the associated content reveals that there are a few extra settings that can be configured that will make the playthrough a little bit more fun and enjoyable.

The other container #

When I went after the debug menu, I only ever touched .pak files - repak pulled DefaultGame.ini straight out of pakchunk0-Windows.pak and that was the unlock. But that’s just a sliver of what the build we have on our hands is capable of.

The configuration file came out clean because .ini files aren’t Unreal assets. They get staged into the .pak as ordinary files rather than cooked packages, so extracting them is somewhat straightforward.

A .uasset, on the other hand, is a completely different animal. Cooked assets live in the IoStore containers - the .utoc and .ucas pairs sitting in the same folder:

...\steamapps\common\Halo Campaign Evolved\Meteorite\Content\Paks\

For those, we will need to use retoc - conveniently, from the same author as repak:

$ retoc unpack pakchunk0-Windows.utoc .\extracted
The same caveats from last time apply - it is a third-party tool, so set your own trust boundaries. It also still needs an Oodle decompressor to do anything useful.

Unpacking pakchunk0 gives you two roots - Engine/ for engine and plugin content, and Meteorite/ for the game itself. The two assets I care about for this particular experiment both sit under Meteorite/Content/UI/Shared/Settings/ in the extracted data:

  • Widgets/WBP_DifficultyModifierMenu.uasset - the menu widget itself.
  • Data/DA_DifficultyModifierSettingsItems.uasset - the list of options it renders.

The second file is a plain data asset that contains the settings the menu builds its content from. Because the menu is configuration-driven, reading that file gets us quite a bit of context. Running strings over it is enough of a starting point to get what we need (we’re flying blind, after all).

Dropdown content #

Just to get ahead of folks thinking this is some kind of official asset parsing process that I am standing up - I am not. Reading a cooked Unreal Engine 5 .uasset properly requires the presence of a .usmap mappings file, which this game doesn’t ship. My goal here is to naively run strings over it and hope for the best. For a data asset that mostly contains names and paths - that turns out to be a fairly decent strategy.

The output we’d get from this is not going to be tidy. UE packs its strings back to back with no separators, so everything interesting arrives as a single blob:

$ strings -a DA_DifficultyModifierSettingsItems.uasset | grep VitalityTraits
@/Game/UI/Shared/Settings/Widgets/Buttons/WBP_MetUI_Dropdown_OptionEntrySettingsUI.Settings.PlayerTraits1VitalityTraits.DamageResistancePercentageSettingVitalityTraits.ShieldRechargeRatePercentageSettingWBP_MetUI_Dropdown_OptionEntry_CWeaponTraits.DamageModifierPercentageSettingWeaponTraits.InfiniteAmmoSettingWeaponTraits.MeleeDamageModifierPercentageSettingHaloUIViewItemDataDA_DifficultyModifierSettingsItemsSettingsViewItemDataDropdownEnumSettingsViewItemDataDropdownInt/Game/UI/Shared/Settings/Data/DA_DifficultyModifierSettingsItems

There’s no delimiter to split on because this isn’t really text in the traditional sense - it’s UE’s name table.

A few of the things here are just scaffolding: asset paths, the bare object name DA_DifficultyModifierSettingsItems, the dropdown row template WBP_MetUI_Dropdown_OptionEntry_C, view-item data classes that say how each row is rendered (HaloUIViewItemData, SettingsViewItemDataDropdownEnum, SettingsViewItemDataDropdownInt), a lone Settings, and the gameplay tag UI.Settings.PlayerTraits1 that the whole screen is bound to.

The ones that we really care about, though, are the following:

VitalityTraits.DamageResistancePercentageSetting
VitalityTraits.ShieldRechargeRatePercentageSetting
WeaponTraits.DamageModifierPercentageSetting
WeaponTraits.InfiniteAmmoSetting
WeaponTraits.MeleeDamageModifierPercentageSetting

Running strings over the aforementioned WBP_DifficultyModifierMenu.uasset turns up the same five trait paths. Seems like we’re on the right path.

Building out the shape #

Notice that the paths are prefixed with VitalityTraits. and WeaponTraits., which means they are fields on some larger structure. To see it in a real, written-to-disk world, open this file:

%LOCALAPPDATA%\Meteorite\Saved\Config\<YourSteamID64>\HaloGlobalGameUserSettings.ini
Back this file up before you touch it. It holds your game settings, and unless you want to reset those from scratch, it’s better to keep an unmodified version of it somewhere.

Under [HaloUserSettings], the file’s only section,1 there are four preset slots. They start out empty:

ModifierPreset=None
PlayerTraits1=(VitalityTraits=(),WeaponTraits=(),MovementTraits=(),AppearanceTraits=())
PlayerTraits2=(VitalityTraits=(),WeaponTraits=(),MovementTraits=(),AppearanceTraits=())
PlayerTraits3=(VitalityTraits=(),WeaponTraits=(),MovementTraits=(),AppearanceTraits=())
PlayerTraits4=(VitalityTraits=(),WeaponTraits=(),MovementTraits=(),AppearanceTraits=())

Unreal writes out the four nested category structs even when every field inside them is carrying only its default value. The individual fields get omitted, which is why you get four sets of empty parentheses. This doesn’t give you the field names you need to modify, but it does hand you the shape for experimentation, and we will experiment here.

For the actual properties we need to modify for each trait, we need to go back to the executable. Because we are tinkering with PlayerTraits, it’s natural to use this as a search term - also with strings (or Select-String if you are on Windows).

Run the following against the Halo: Campaign Evolved executable in the game folder:

Select-String -Path "HaloCampaignEvolved.exe" -Pattern "\w*PlayerTraits\w*" -Encoding ascii -AllMatches |
  ForEach-Object { $_.Matches.Value } | Sort-Object -Unique

Or, if you have Windows Subsystem for Linux (WSL), Git Bash, or you’re actually on Linux or macOS like myself:

$ strings -a HaloCampaignEvolved.exe | grep -oE '\w*PlayerTraits\w*' | sort -u

Either way, you will see the following output:

BlamGameEnginePlayerTraits
GetPerPlayerTraits
HasModifiedPlayerTraits
PerPlayerTraits
PlayerTraits
PlayerTraits1
PlayerTraits2
PlayerTraits3
PlayerTraits4
SetPerPlayerTraits

The winning choice for us is BlamGameEnginePlayerTraits. Assuming that Blam-related items are prefixed with, well, Blam, we can take this a step further and see if we can find references to the player traits which are labeled the same way:

Select-String -Path "HaloCampaignEvolved.exe" -Pattern "BlamPlayerTrait\w+" -Encoding ascii -AllMatches |
  ForEach-Object { $_.Matches.Value } | Sort-Object -Unique

Or, for bash aficionados:

$ strings -a HaloCampaignEvolved.exe | grep -oE 'BlamPlayerTrait\w+' | sort -u

This will yield us the following list:

BlamPlayerTraitAppearance
BlamPlayerTraitMovement
BlamPlayerTraitVitality
BlamPlayerTraitWeapons

Well isn’t this a coincidence - it matches what we saw earlier in the config file.

The individual fields follow the convention too. Their enum types are prefixed EBlam, which is a far cleaner thing to search for (I can only assume E stands for Enum, duh!):

Select-String -Path "HaloCampaignEvolved.exe" -Pattern "EBlam\w*Setting" -Encoding ascii -AllMatches |
  ForEach-Object { $_.Matches.Value } | Sort-Object -Unique

And, in bash:

$ strings -a HaloCampaignEvolved.exe | grep -oE 'EBlam[A-Za-z]*Setting' | sort -u

We will now be in possession of a list like this:

EBlamActiveCamoSetting
EBlamDamageModifierPercentageSetting
EBlamDamageResistancePercentageSetting
EBlamInfiniteAmmoSetting
EBlamPlayerGravitySetting
EBlamPlayerSpeedSetting
EBlamRechargeRatePercentageSetting
EBlamRenderSetting

Ignore EBlamRenderSetting - it’s the regex biting a prefix off EBlamRenderSettingsChangeType, but the rest are types behind trait fields, and three of them describe things the Difficulty Modifiers menu has never once offered you:

  • EBlamPlayerSpeedSetting
  • EBlamPlayerGravitySetting
  • EBlamActiveCamoSetting

That regex doesn’t catch everything, though. Three fields (DeathlessSetting, WeaponPickupSetting, RechargingGrenadesSetting) share a single type called EBlamBooleanTrait, and ModifierPreset uses EModifierPresetSetting with no Blam prefix at all. We treat the convention is a useful lead rather than a generic rule.

Types are only half of it. We need the enumerators, and to the surprise of nobody we can extract them from the binary too, in Type::Value form:

Select-String -Path "HaloCampaignEvolved.exe" -Pattern "EBlamPlayerSpeedSetting::\w+" -Encoding ascii -AllMatches |
  ForEach-Object { $_.Matches.Value } | Sort-Object -Unique

For our bash friends:

$ strings -a HaloCampaignEvolved.exe | grep -oE 'EBlamPlayerSpeedSetting::[A-Za-z0-9]+' | sort -u

Now take a look here:

EBlamPlayerSpeedSetting::Num
EBlamPlayerSpeedSetting::Percent0
EBlamPlayerSpeedSetting::Percent100
EBlamPlayerSpeedSetting::Percent110
EBlamPlayerSpeedSetting::Percent120
EBlamPlayerSpeedSetting::Percent130
EBlamPlayerSpeedSetting::Percent140
EBlamPlayerSpeedSetting::Percent150
EBlamPlayerSpeedSetting::Percent160
EBlamPlayerSpeedSetting::Percent170
EBlamPlayerSpeedSetting::Percent180
EBlamPlayerSpeedSetting::Percent190
EBlamPlayerSpeedSetting::Percent200
EBlamPlayerSpeedSetting::Percent25
EBlamPlayerSpeedSetting::Percent300
EBlamPlayerSpeedSetting::Percent50
EBlamPlayerSpeedSetting::Percent75
EBlamPlayerSpeedSetting::Percent90
EBlamPlayerSpeedSetting::Unchanged

There are twelve trait fields in total. There are seven with no option to set from the menu:

ActiveCamoSetting
BodyRechargeRatePercentageSetting
DeathlessSetting
GravitySetting
RechargingGrenadesSetting
SpeedSetting
WeaponPickupSetting

So MovementTraits holds SpeedSetting and GravitySetting, and AppearanceTraits holds ActiveCamoSetting. Neither category has a dropdown anywhere in the game. Within the two categories the menu does show, it skips DeathlessSetting, BodyRechargeRatePercentageSetting, WeaponPickupSetting, and RechargingGrenadesSetting. But this, of course, doesn’t mean we can’t set these properties.

Secret traits not so secret anymore #

Back in HaloGlobalGameUserSettings.ini, we can now start adding some of the things that we just discovered.

Our payload will touch only fields with no dropdown. We need to close the game first, since it rewrites this file on exit and will stomp whatever we typed. Then, we will replace the ModifierPreset and PlayerTraits1 lines in place with this blob:

ModifierPreset=Preset01
PlayerTraits1=(VitalityTraits=(DeathlessSetting=On,BodyRechargeRatePercentageSetting=Percent200),WeaponTraits=(WeaponPickupSetting=On,RechargingGrenadesSetting=On),MovementTraits=(SpeedSetting=Percent300,GravitySetting=Percent50),AppearanceTraits=(ActiveCamoSetting=Invisible))

We can now launch the game and start a new mission. You will know things are taking effect if you go into the Difficulty Modifiers view and see Preset 01 as the selected preset.

You are now moving at triple speed, jumping in half gravity, and fully invisible. Every one of those is as far as its enum goes. Percent300 tops the speed ladder, Invisible tops the camo one, and Percent50 is the floatiest gravity we can get.

The payload also sets DeathlessSetting, BodyRechargeRatePercentageSetting, WeaponPickupSetting and RechargingGrenadesSetting, because why not.

This very likely will interfere with achievements. BlamAchievementDefinition in the executable carries a bBlockedByDifficultyModifiers flag, sitting alongside RequiredActiveSkulls and BlockerActiveSkulls, and the binary also exposes a HasModifiedPlayerTraits check. I have not confirmed the behavior, but setting ModifierPreset to anything other than None looks very much like the switch that trips it. Set it back to None if you care about achievements.

Values with no menu entry #

Most typical dropdown options are enumerated in the same UI data asset. Those are the values the menu can produce and we can see them in-game.

The underlying enums, however, go further:

Setting Menu limit Extra value
DamageResistancePercentageSetting Percent2000 Invulnerable
DamageModifierPercentageSetting Percent300 Fatality
ActiveCamoSetting No menu entry Up to Invisible

Which lets you write this:

ModifierPreset=Preset01
PlayerTraits1=(VitalityTraits=(DamageResistancePercentageSetting=Invulnerable,ShieldRechargeRatePercentageSetting=Percent200,BodyRechargeRatePercentageSetting=Percent200,DeathlessSetting=On),WeaponTraits=(DamageModifierPercentageSetting=Fatality,MeleeDamageModifierPercentageSetting=Fatality,InfiniteAmmoSetting=BottomlessClip,WeaponPickupSetting=On,RechargingGrenadesSetting=On),MovementTraits=(SpeedSetting=Percent300,GravitySetting=Percent50),AppearanceTraits=(ActiveCamoSetting=Invisible))

Who wouldn’t want to play with triple speed, half gravity, invisible, invulnerable, one-shot kills and bottomless magazines!

Two limits to keep in mind:

  • Percent300 is the top speed. The ladder tops out at that value, at least from what I can tell looking at existing reverse-engineered values.
  • Percent50 is the lowest gravity. Unfortunately, it doesn’t seem like there is another lever for hang time.

The Halo engine underneath all of this ships as its own DLL, HaloSimulation_tag_release.dll, sitting in the same Binaries\Win64 folder. Blam tags describe their own fields, so those descriptions survive into the binary as plain strings - name, a #, then the help text:

$ strings -a HaloSimulation_tag_release.dll | grep -i 'unable to jump'
jump multiplier#use -1 for 'unchanged.'  0 will make the player unable to jump. 100 is default.

So a jump trait exists, it is separate from gravity, and 100 is its baseline - which also confirms these multipliers read the way you’d hope, with Percent50 gravity meaning floatier rather than heavier. The same dump has speed multiplier and gravity multiplier as sibling labels, and no jump multiplier equivalent anywhere in BlamPlayerTraitMovement, which exposes only SpeedSetting and GravitySetting.

The extracted asset package confirms what SpeedSetting actually drives. Under Meteorite/Content/Tags/multiplayer/game_variant_settings/player_traits_template/ the movement traits ship as four separate tags:

  • traits_movement_walking_speed
  • traits_movement_personal_gravity
  • traits_movement_jump_height
  • traits_movement_vehicle_useage

Walking speed and jump height are distinct traits. So SpeedSetting buys us horizontal movement, and the trait that would buy hang time is not wired up to anything we can set. Alas, maybe I am just missing some obvious option - do tag me in the comments if you find one. Let’s get back to testing, though.

Mole is thinking.

Is there a way to check whether my edit took, short of loading a mission and guessing?

The easiest way to test is to just launch the game. I know, very obvious - but you will instantly see the effect. I use Truth and Reconciliation as a test mission and you will see that I am camo-d up right from the start, with absurd speed, taking zero damage, and floating in the air.

Permanent camo mod in a Halo: Campaign Evolved mission.
Permanent camo mod in a Halo: Campaign Evolved mission.

See it in action:

Full value reference #

Now that you know the basics, I thought I’d also provide a dump of all the values that you can set yourself. Use this at your leisure.

Preset selector #

Field Accepted values
ModifierPreset None, Preset01, Preset02, Preset03, Preset04

None is the shipped default - it means that nothing is applied.

VitalityTraits #

Field In menu Accepted values
DamageResistancePercentageSetting Yes Unchanged, Percent10, Percent50, Percent90, Percent100, Percent110, Percent150, Percent200, Percent300, Percent500, Percent1000, Percent2000, Invulnerable
ShieldRechargeRatePercentageSetting Yes Unchanged, PercentNegative25, PercentNegative10, PercentNegative5, Percent0, Percent10, Percent25, Percent50, Percent75, Percent90, Percent100, Percent110, Percent125, Percent150, Percent200
BodyRechargeRatePercentageSetting No Unchanged, PercentNegative25, PercentNegative10, PercentNegative5, Percent0, Percent10, Percent25, Percent50, Percent75, Percent90, Percent100, Percent110, Percent125, Percent150, Percent200
DeathlessSetting No Unchanged, Off, On
Mole is thinking.

Does a higher number here mean I take less damage, or more? Percent2000 could be read either way.

Less. The field is resistance, not damage taken, so higher is tougher - the menu tops out at Percent2000, and Invulnerable is one rung above it.

BodyRechargeRatePercentageSetting is health regeneration as opposed to shields.

DeathlessSetting is a plain boolean, and the engine’s trait variable table groups it with player headshot immunity, player assassination immunity and player vampirism, with a matching cheat_deathless_player debug command sitting nearby - so it should stop you dying.

WeaponTraits #

Field In menu Accepted values
DamageModifierPercentageSetting Yes Unchanged, Percent0, Percent25, Percent50, Percent75, Percent90, Percent100, Percent110, Percent125, Percent150, Percent200, Percent300, Fatality
MeleeDamageModifierPercentageSetting Yes Unchanged, Percent0, Percent25, Percent50, Percent75, Percent90, Percent100, Percent110, Percent125, Percent150, Percent200, Percent300, Fatality
InfiniteAmmoSetting Yes Unchanged, Off, On, BottomlessClip
WeaponPickupSetting No Unchanged, Off, On
RechargingGrenadesSetting No Unchanged, Off, On

The damage menus stop at Percent300. For ammo, On stops it draining and BottomlessClip also removes reloading.

RechargingGrenadesSetting refills grenades over time.

WeaponPickupSetting is the one field here I can’t tell you the effect of. The older Blam tag definitions inside HaloSimulation_tag_release.dll carry it as a plain weapon pickup field in player_trait_weapons_block, and the engine has a player_disable_weapon_pickup script call sitting next to things like player_disable_movement - which reads like a permission toggle for whether you can pick weapons up at all, rather than a buff.

MovementTraits #

Field In menu Accepted values
SpeedSetting No Unchanged, Percent0, Percent25, Percent50, Percent75, Percent90, Percent100, Percent110, Percent120, Percent130, Percent140, Percent150, Percent160, Percent170, Percent180, Percent190, Percent200, Percent300
GravitySetting No Unchanged, Percent50, Percent75, Percent100, Percent110, Percent120, Percent130, Percent140, Percent150, Percent160, Percent170, Percent180, Percent190, Percent200

AppearanceTraits #

Field In menu Accepted values
ActiveCamoSetting No Unchanged, Off, Poor, Good, Excellent, Invisible

And just as a final note, PlayerTraits2 through PlayerTraits4 are preset slots 2 to 4, selected with ModifierPreset=Preset02, ModifierPreset=Preset03 and ModifierPreset=Preset04 respectively, if you want to have some supersoldier custom setups that you want to use on other missions.

Conclusion #

Because this is the first public build, I am not certain that what you see above will last. At the same time, I am excited to continue digging and see what other settings I will find. I am sure there is more hiding in plain sight.


  1. This is an exception to something I said last time. The debug menu post claimed a config class’s section header must be [/Script/<Module>.<Class>], and this one is plainly [HaloUserSettings]. Some classes override the section name, and this is one of them - the rule holds everywhere it isn’t overridden, which is nearly everywhere. ↩︎

Unlocking The Halo: Campaign Evolved Secret Debug Menu

2026-07-25 08:00:00

I still vividly remember the day at the Halo World Championship 2025, when Halo Studios announced Halo: Campaign Evolved. New graphics, more missions, and a dive towards Unreal Engine instead of Slipspace. Lots to be excited about!

Naturally, when the game first became available, I just had to get it and start playing through the campaign. Now, despite the fact that some folks can complain about the fact that there is no multiplayer, this was not a major detractor for me - I’ve always been a campaign-first kind of guy.

But if you know me, you also know that I love looking under the hood of the released Halo games - just think of how much time I spent on random Halo API explorations. When I got the build installed on my local machine, the first question I had was “What can I tinker with in this game?

Halo: Campaign Evolved loading screen.
Halo: Campaign Evolved loading screen.

You can watch the video if you want a more hands-on explanation:

Unreal Engine rules everything around me #

The first stop was, of course, the install folder. Halo: Campaign Evolved sits in your Steam library under steamapps/common/Halo Campaign Evolved, and the moment you open it you can tell what you’re dealing with:

Halo Campaign Evolved/
├── Engine/
├── DigitalExtras/
└── Meteorite/
    ├── Binaries/Win64/HaloCampaignEvolved.exe
    └── Content/Paks/

That Meteorite folder is the interesting part - it’s clearly the codename for the game, and it shows up everywhere (including the REST APIs, where it’s referred to as mtr). The build string baked into the executable spells the whole thing out, if you had any doubts:

5.5.4-2026.06.26.1097863.1-Rel-i343-Meteorite-2606-CU2

Unreal Engine 5.5.4 (I am pretty positive that’s what this is, but it’s conjecture), built on June 26, changelist 1097863, from a branch called Rel-i343-Meteorite. The i343 prefix is also unsurprisingly popping up everywhere - you’ll see paths like Engine/Plugins/i343/BlamEngine scattered through the binary, which is a delightful detail if you are a diehard Halo fan. The classic Halo “Blam” engine (or, whatever pieces of it remain - I have no clue) now lives as an Unreal plugin.

With UE, all the game content lives in Meteorite/Content/Paks, and there’s a lot to unpack (hah!) there. A keen eye will spot two file types:

For my exploration, I started with .pak files only - that was more than enough, as it turns out. To extract content from it, I used repak from Truman Kilen.

Mole is thinking.

This is a third-party tool, right? Is it OK for me to use it on my machine?

While these tools do what they were designed to do and I have no reason to believe they are in any way, shape, or form malicious, I cannot vouch for their safety and reliability! As with any external project (mine included, by the way), always exercise caution and define the trust boundaries where you want to run them. When I was experimenting with extraction using repak, I ran everything through isolated containers with mounted data folders.

repak needs an Oodle decompressor to do anything useful, since that’s what the containers are compressed with. You can often get the required decompressor library from Steam games that bundle it in the distribution themselves.

The file that I wanted to unpack first is pakchunk0-Windows.pak (roughly two and a half GB in size). Because I saw that quite a few settings in the game in %LOCALAPPDATA% and within its own folder uses *.ini, I decided to intentionally narrow down my search to those files.

One repak list later:

$ repak list pakchunk0-Windows.pak | grep -i '\.ini$' | wc -l
145

That means there are 145 configuration files, in plain text, inside the PAK. Including one that caught my attention right away:

Meteorite/Config/DefaultGame.ini

My hunch was that it contained some default game state that controls its general behavior. Because I have no intimate knowledge of how everything works here, this seemed like a reasonable starting point, so I extracted it.

$ repak get pakchunk0-Windows.pak "Meteorite/Config/DefaultGame.ini" > DefaultGame.ini

This resulted in 27 KB of internal settings. There’s a lot there that I won’t really list here, since it’s mostly irrelevant to what I wanted to talk about in this blog post. However, about two thirds of the way down, this came up:

[/Script/Meteorite.DebugMenuSettings]
bEnableDebugMenuBetaNonShipping=True
bEnableDebugMenuReleaseNonShipping=True

I wonder what this would do? Look at those two key names, but split them into logical parts:

Anatomy of the shipped debug menu flags Both flags in the shipped DefaultGame.ini end in NonShipping, so neither one applies to a retail Shipping build.bEnableDebugMenuBetaNonShipping= TruebEnableDebugMenuReleaseNonShipping= TrueNot your retail build

A pretty good guess here is that there are different builds of the game available - Debug, Development, Test, Shipping. The retail copy you buy is a Shipping build. And the game’s own config only enables the debug menu for NonShipping builds.

Which raises the obvious question: if there’s a NonShipping variant of these flags, is there a Shipping one? You don’t need any special tooling to answer that - the flag names are sitting in the game executable as plain ASCII. Point PowerShell at it:

Select-String -Path "HaloCampaignEvolved.exe" -Pattern "bEnableDebugMenu\w+" -Encoding ascii -AllMatches |
  ForEach-Object { $_.Matches.Value } | Sort-Object -Unique
Finding the needle in the haystack of binary strings.
Finding the needle in the haystack of binary strings.

That’s a bingo:

bEnableDebugMenuBetaNonShipping
bEnableDebugMenuBetaShipping
bEnableDebugMenuDefaultNonShipping
bEnableDebugMenuDefaultShipping
bEnableDebugMenuReleaseNonShipping
bEnableDebugMenuReleaseShipping

Every build configuration gets a pair - one for non-shipping builds and one for shipping. The config file sets two of them, both on the NonShipping side. The other four, including every single Shipping variant, aren’t mentioned anywhere in the shipped configuration, so they quietly fall back to their default of False. I can probably try and override that behavior.

One INI file at a time #

Because the INI file I was looking at is packaged along other assets, it can’t be edited in place. Now, repacking the container to flip one boolean would likely be possible, but I wanted to exhaust all easy options first.

Mole thinking.

Wait, wait, wait… Hold on. Why not just repack the PAK? The files aren’t encrypted or signed, so nothing is stopping you from writing a modified DefaultGame.ini back into the container.

I mean - I guess you could? But we probably have a much cleaner path available. The config system is layered - the engine reads a whole hierarchy of .ini files in order, and later layers override earlier ones. The PAK-staged DefaultGame.ini is one of the earlier layers. The last layer, the one that wins, is the writable user directory on my own machine.

For this version of Halo, that location is the following:

%LOCALAPPDATA%\Meteorite\Saved\Config\Windows\

If you’ve launched the game at least once, that folder already exists and already has a GameUserSettings.ini in it holding some of the settings. The game reads and writes it constantly. Recall how I mentioned that I focused on INI files first? This is why.

I’ll call out an important INI formatting rule that I discovered in the process: an Unreal config class reads from the .ini file that matches its config= specifier, and its section header must be [/Script/<Module>.<Class>].

If you get either one wrong - the setting won’t be interpreted correctly and nothing will happen.Because I don’t have access to verbose logs, I couldn’t really tell if something is off until I tinkered with the files and got the setup right by restarting the game a million times.

DebugMenuSettings appeared in DefaultGame.ini, so it’s a Game config class, so the override belongs in Game.ini. Same folder as GameUserSettings.ini.

To make the change, close the game first. Then, create %LOCALAPPDATA%\Meteorite\Saved\Config\Windows\Game.ini and put this in it:

[/Script/Meteorite.DebugMenuSettings]
bEnableDebugMenuDefaultShipping=True
bEnableDebugMenuBetaShipping=True
bEnableDebugMenuReleaseShipping=True
bEnableDebugMenuDefaultNonShipping=True
bEnableDebugMenuBetaNonShipping=True
bEnableDebugMenuReleaseNonShipping=True

Right-click on the file and mark it as read-only, to make sure that the game doesn’t stomp over it.

Marking a game configuration file as read-only.
Marking a game configuration file as read-only.

That’s it - the magic is in. Let’s launch the game!

The menu #

When you go past the launch screen, you will now see the option to get the debug controls:

Debug options now available in Halo: Campaign Evolved.
Debug options now available in Halo: Campaign Evolved.

See that G Toggle Debug Options at the bottom? That means the configuration took effect. And now, you can do a whole bunch of really fun things.

And of course, you can do some more fancy fancy things, like testing campaign missions or some maps/game variants, that are somehow inaccessible (I haven’t spent enough time digging through this, maybe there is another secret flag).

Now that’s an Easter Egg!

MCP Apps And Interactive UIs In MCP Clients

2026-01-26 08:00:00

For the past year, Model Context Protocol (MCP) has done a lot of maturing - starting as a small open source experiment, it now became a full-fledged protocol that is broadly adopted by the industry at large. You can throw a rock and hit something that somehow integrates MCP.

That being said, one of the more peculiar limitations of MCP has always been the fact that you can’t really do a lot with it beyond text on the wire (which is still extremely useful, to be very clear) - JSON-RPC is a nice little abstraction to ferry a bunch of text-based requests and responses. Anything interactive was usually done with the help of, what I would say, hacks, like sending the URL that a user would have to go to to see a visualization of their data or results of some action (think - Playwright MCP post-test reports).

UI Comes To MCP #

Starting today, though, the landscape is changing - MCP took another leap by adding support for its first official extension, MCP Apps, a project built on the foundations of the work that the awesome folks at MCP-UI and OpenAI have been carefully nurturing.

You can catch up with my demo video to see it in action:

What’s cool about MCP Apps is that you can already tinker with it in Claude and Visual Studio Code Insiders, with more clients on the way!

Here are a few places for you to check out if you want to get building:

API Documentation Quickstart

And of course, I would be remiss if I didn’t call out the blog post announcing the release as well as the extensive collection of samples that will show you how to get started quickly.

MCP Apps are nothing other than HTML layered on top of the existing protocol abstractions, so the changes for both server and client implementers are fairly surgical. This will also hopefully make it much easier to adopt within the ecosystem.

For those reading who are a bit more security conscious, I got you covered - because apps run inside a sandboxed iframe controlled by the host, developers don’t have to worry about them escaping their container, accessing the parent page, or doing anything nefarious with cookies.

But - and that’s a big one, the real magic here is the bidirectional flow of data. An MCP App can invoke server tools and receive live updates without developers having to spin up separate infrastructure or deal with transport plumbing themselves. Neat!

A sample MCP App inside Claude AI, running on Ubuntu Linux.
A sample MCP App inside Claude AI, running on Ubuntu Linux.

Come Contribute #

The MCP Apps extension is, of course, still under active development. If you are still on the fence if you want to help shape the direction of the protocol, now is the perfect time to get involved.

For bugs or feature requests, the team is tracking everything through GitHub Issues.

For broader discussions about where the project should go or how it fits into existing workflows, there’s a dedicated space in GitHub Discussions.

And hey, if you want to take my biased opinion, this is one of those rare moments where the extension setup is still malleable enough that community feedback can genuinely shape its direction. Come help!

Programmatically Setting GitHub Issue Types

2026-01-17 08:00:00

As an open source project maintainer, one of the things that I often need to do, to the surprise of no one, is triage issues. When doing so, I try to rely as much as possible on automation; however, it’s not always available out-of-the-box for some edge cases.

One such edge case is setting issue types in GitHub. If you are not familiar with it, I am not talking about issue labels, but specifically types.

Issue types in the GitHub web UI.
Issue types in the GitHub web UI.

Typically, I’d use the GitHub CLI for this kind of toil, but as it turns out there is no argument available that allows me to set the type. So, I had to look for creative workarounds.

Under the issue type hood #

Issue types are entirely owner-managed. For example, in the PowerToys repo (where I maintain Awake), a maintainer can flag something as a Bug, Feature, or Task.

Now, if I’d ask anyone how they can set types for a given issue, they’d probably guess that they can use gh issues edit, but alas that’s not something I can do. Luckily, the GitHub CLI allows me to also talk directly to the GitHub GraphQL API, which offers way more capabilities than the command line tool.

So, I’ll start by querying the available issue types for the aforementioned PowerToys repository:

gh api graphql -f query='
  {
    repository(owner: "microsoft", name: "powertoys") {
      issueTypes(first: 20) {
        nodes {
          id
          name
          description
        }
      }
    }
  }'

This will yield a JSON blob like this:

{
  "data": {
    "repository": {
      "issueTypes": {
        "nodes": [
          {
            "id": "IT_kwDOAF3p4s4ACCgE",
            "name": "Task",
            "description": "A specific piece of work"
          },
          {
            "id": "IT_kwDOAF3p4s4ACCgH",
            "name": "Bug",
            "description": "An unexpected problem or behavior"
          },
          {
            "id": "IT_kwDOAF3p4s4ACCgK",
            "name": "Feature",
            "description": "A request, idea, or new functionality"
          }
        ]
      }
    }
  }
}

Not bad. I now have the unique id associated with each issue type. But I don’t just need to get the issue types. I need to be able to set them. To do that, I’m once again going to lean on the GitHub GraphQL API, with the help of mutations. The GitHub GraphQL API uses global node IDs and not the issue numbers (what you see in the web UI) for mutations, meaning I need to retrieve the issue’s node ID first.

Here is the GraphQL query that I need to execute from the GitHub CLI:

gh api graphql -f query='
{
  repository(owner: "microsoft", name: "powertoys") {
    issue(number: 44644) {
      id
      title
      issueType {
        name
      }
    }
  }
}'

If all goes well, this is what I’ll get:

{
  "data": {
    "repository": {
      "issue": {
        "id": "I_kwDOCv6UO87iZtnQ",
        "title": "Bug report run logs",
        "issueType": {
          "name": "Bug"
        }
      }
    }
  }
}

The id value is all I need. Now, I can use the updateIssue mutation to set the type.

gh api graphql -f query='
mutation {
  updateIssue(input: {
    id: "I_kwDOCv6UO87iZtnQ",
    issueTypeId: "IT_kwDOAF3p4s4ACCgH"
  }) {
    issue {
      number
      title
      issueType {
        name
      }
    }
  }
}'

Notice the issueTypeId - this is where I use the relevant issue type ID from the very first step to set the type.

A successful type assignment operation will result in this JSON output in your terminal:

{
  "data": {
    "updateIssue": {
      "issue": {
        "number": 44644,
        "title": "Bug report run logs",
        "issueType": {
          "name": "Bug"
        }
      }
    }
  }
}

To remove an issue type from an issue, pass null as the issueTypeId in the GraphQL query above.

Conclusion #

It’s a bit more work than I’d like for something this basic, but wrapping these queries in a shell script or a custom CLI extension (that’s for another blog post) makes it easy to integrate into your triage workflow. Hopefully native gh issue edit support comes soon.

Hello, Anthropic

2026-01-11 08:00:00

Just a few days ago I wrote about wrapping up my latest Microsoft chapter. I’ve spent the past three years immersed in security, and as of last year alone, quite a bit of Model Context Protocol (MCP). That last part is probably eye-roll-inducing to anyone who’s been following me on LinkedIn.

This work serendipitously led me to what comes next. My next chapter is joining Anthropic (yes, that Anthropic) as a Member of Technical Staff.

A new year, a new start - as relayed to us by the famous Calvin and Hobbes.
A new year, a new start - as relayed to us by the famous Calvin and Hobbes.

Before we go further, I just wanted to mention how much I love the work behind Calvin and Hobbes. This particular strip from the very last installment of the comic (published on December 31st, 1995) feels especially fitting for this moment. There’s something different about stepping into a new role at the start of a new year - a blank canvas, much like a coat of fresh snow (which, apparently we’re not getting much of in the PNW this year).

The switch to Anthropic was not super-spontaneous. After a year working on MCP as a member of the MCP Steering Committee and then a Core Maintainer focused on auth and security, I really grew to appreciate the effort that Anthropic was putting into organically scaling what has now become an industry standard for connecting data and applications to Large Language Models (LLMs). I also had a few informal conversations with Anthropic folks, learning about their roadmap, culture, and aspirations.

Then, a chance to work closely with the Anthropic MCP crew came up. The opportunity to help build MCP from inside felt like such a surprisingly natural fit that I did a double take and asked my wife, “Is this even a real job?” As it turns out, it was! It also helps that I’m already a huge fan of the Claude family of models (my go-to for all engineering work), so the decision to go work with folks whose product I am already using practically daily was easy.

Ultimately, the decision to join was grounded in a few core beliefs that I hold:

  • Mission over hype. The AI space is full of noise, work not based on human need (and sometimes straight-up malicious), and assertions not grounded in reality. Anthropic’s focus on building AI responsibly, with safety as a first-class concern, and doing so in a way that is producing genuinely useful outcomes is exactly how I think this technology should be built.
  • Kindness, ethics, and empathy matter. It struck me just how kind, friendly, and mission-driven the people at Anthropic were. This is huge for me, and seeing it recognized even by folks outside the company was incredibly exciting.
  • Bet on the frontier. I want to work on things at scale that push boundaries. Anthropic is that place for AI right now, and I want to help shape what comes next - in a way that benefits humanity.
  • Make a dent. I want my work to have a positive impact - not just ship features, but genuinely help folks build better software and solve harder problems.
No pressure (at all)!
No pressure (at all)!

At Anthropic, my immediate focus will be, you guessed it, on MCP. If you’re already part of the contributor community, I’m sorry, but you’ll be seeing more of me. If you’re part of the broader MCP ecosystem, I am excited to collaborate with you on making the protocol even more mature!

As the world moves to agent-first workflows, I realize that there is so much more to be done to nurture and grow MCP for this paradigm shift. I strongly believe that Anthropic is the place to do it - not just because they created MCP, but because they’re deeply committed to building AI with safety in mind. As agents gain more autonomy and connect to more systems, that commitment becomes non-negotiable.

This career change is a calculated bet on the impact of AI on the world, and specifically on something near and dear to me: software engineering and the need to connect systems to make them work well together (which, as you know, is the whole idea behind MCP). As models become ubiquitous “appliances” in modern software, it will be more important than ever to make sure they interoperate safely, securely, and in a scalable way with other systems, services, APIs, and applications. My mental calculus here was simple: even if I can contribute just a tiny bit to accelerate MCP adoption, help improve its security posture and readiness for more real-world scenarios, there is no better place to do that than at its birthplace.

I am also excited to work more closely with David Soria-Parra, Jerome Swannack, Paul Carleton, Basil Hosmer, Inna Harper, Weynab Maher, and quite a few others at Anthropic who are building out the protocol and the broader agentic AI ecosystem.

I guess the Claude motto of "Keep thinking." will be even more relevant now.
I guess the Claude motto of “Keep thinking.” will be even more relevant now.

It’s hard to put into words how I feel about this change - I am nervous and excited, cautiously optimistic and yet ready to jump in and start contributing. I am positive that working at Anthropic will be transformative, and I am looking forward to both learning from some of the most talented folks in the industry as well as helping steer us to a future where AI is safe, accessible, and beneficial at scale.

Let’s shucking go!

Wrapping Up My Latest Microsoft Chapter

2026-01-09 08:00:00

Three years and a quarter ago, my hiring manager asked me - “How do I know you’re not going to leave Microsoft again if we hire you?” It’s not an unfair question to ask, especially considering that I was going for my third stint at the company. My answer to that was pretty simple, at least from my vantage point - I can never guarantee this, but I will give it my all to build the best product imaginable because I live and breathe developer experience.

Three years and a quarter - that’s how long it took me to decide that it was time to close this chapter of my Microsoft adventure. Today is my last day at the company, and next week I’ll be diving headfirst into a new challenge.

The skybridge between Building 16 and Building 18 in Redmond, WA.
The skybridge between Building 16 and Building 18 in Redmond, WA.

Developer Division & CoreAI #

It’s more than a bit of a bittersweet moment, because the team and the role I was in were fantastic. After being there for the past year (the previous two I spent working in Microsoft Security), I can tell you that Developer Division, or DevDiv as folks called it before it got folded into CoreAI, is an extremely fun place to work. I’m not just saying this to be nice - it really is the place to be at Microsoft to ship fast, learn fast, and be on the cutting edge of what developers use every day.

Send-off dinner with some awesome folks on the Microsoft campus.
Send-off dinner with some awesome folks on the Microsoft campus.

This was my dream org since I joined Microsoft. We’re talking about the birthplace of Visual Studio, Visual Studio Code, C# (and the .NET platform), TypeScript, Visual Basic (that’s what I started with way, way back), and so much more. Could an absolute engineering geek ask for a better place to be?

Boxed versions of Visual Studio, courtesy of Simon Calvert.
Boxed versions of Visual Studio, courtesy of Simon Calvert.

It’s something I always aspired to be a part of, and through a mix of luck and a whole lot of very much unrelated and very much unexpected stress, this dream became reality in the very first weeks of 2025.

Since January of last year I had the privilege of tackling many interesting problems, like helping figure out the adoption blockers for GitHub Copilot, charting out the path for better authentication and authorization integration in our IDEs (I guess I put my Entra ID knowledge to use here too), building quite a few conference prototypes and demos, training the internal Model Context Protocol (MCP) security muscle, and most recently - launching GitHub Spec Kit, which blasted past 61,000 stars on GitHub. Like I said, this org is lots of fun, and I had a lot of agency to do things that are impactful.

Oh yeah, and did I mention that GitHub Spec Kit is the second most starred repository in the entire GitHub org? Talk about a little experiment getting out of hand.

But as with any career step, sometimes the chutes and ladders drop us in wildly unexpected directions. That’s exactly what happened here. The timing is a bit odd, considering that just this fall I shifted roles, but I knew deep down that the change was something I had to do - at least to try and apply my skills at a different scale. I’ve done moves like this in the past, but this one feels particularly poignant - and yet, very exciting.

Acknowledgements #

I wanted to take a moment in this somewhat long-winded blog post to express my immense gratitude to a few folks who were absolutely instrumental to my career in the past few years and even way before then.

It would be a grave disservice to not first call out just how impactful the work of Amanda Silver is on everything that I did at Microsoft and even outside of it before coming to DevDiv. People might not know just how big of an influence she is on cultural, technical, and product direction in Microsoft’s developer ecosystem - she truly is second to none when it comes to understanding developers. In my eyes, Amanda is the Developer Division. I owe her my position in DevDiv and CoreAI as a whole - she did the most for me of any managers or mentors in the past half decade, and I know a lot of folks who feel the same way. My biggest reservation about leaving Microsoft was that I won’t get to work with Amanda on the same team.

A few other folks that I want to individually call out for all they did to help shape my latest Microsoft tour of duty:

  • Simon Calvert, who provided a whole new perspective on what product leadership really stands for.
  • John Lam, because GitHub Spec Kit would not happen without his work and research. When I think of someone who thinks outside the box, John is at the top of that list.
  • Jeff Wilcox, who is always a voice of reason I can count on in the most uncertain moments.
  • Clint Rutkas, who brought me to Microsoft and continued to be one of my best friends and trusted advisors for more than a decade now.
  • James Montemagno, a ray of positivity and optimism who is best known for his “Let’s do it!” attitude.
  • Scott Hanselman, who needs no introduction. His advice has guided me since his early podcast episodes and through my Microsoft career.
  • Scott Hunter, who didn’t shy away from sticking his neck out for me in some of the toughest situations.
  • Brian Peek, who provided a good dose of realism almost every day.
  • Brady Gaster, who provided friendly support and lots of product suggestions that steered my projects in way better directions.
  • Caitie McCaffrey, arguably the most impactful internal MCP champion who knows about distributed systems more than anyone I know.
  • Henrik Metzger, the fearless leader of the Microsoft Identity Service Essentials (MISE) engineering org, who helped me build a much better understanding of the security world.
  • Jenny Ferries, who was never afraid to say the right thing, and most importantly, do the right thing.
  • Jackson Davis was my go-to conversation partner in the past year, and I think he secretly is the most interesting man in the world.
  • Jean-Marc Prieur, the only person I know who knows more about identity than the entire security org.

Also, very special shout-out goes also to some of the most amazing folks that I had the privilege of crossing paths with, who more than once helped me beyond what one could even ask for: Julia Kasper, Annaji Sharma Ganti, Tyler Leonhardt, Mandy Whaley, Scott McMurray (Halo Studios), Diviyan Matheendran (Halo Studios), Nancy Anderson, Jeff Carnahan (Halo Studios), Toby Padilla, Mike Kistler, Lutz Roeder, Josh Free, Peter Marcu, Peter Maytak, Gladwin Johnson, Neha Bharghava, Keegan Caruso, Josh Lozensky, Kelly Song, Bogdan Gavril, Ray Luo, Travis Walker, Adrian Frei, Iulian Cociug, Chris Mann, Christopher Scott, Saeed Akhter, Stephen Halter, Stephen Toub, David Fowler, Joe Binder, Paul Yuknewicz, Shayne Boyer, Maddie Montaquila, Pierce Boggan, Maria Naggaga, Ernie Booth, Cassie Breviu, Eric Hollenbery, Hemory Phifer, Matt Ellis, Matthew Reyermann, Martin Woodward, Mario Rodriguez, Chuck Lantz, Tim Heuer, Denizhan Yigitbas, Evan Boyle, and a massive fleet of other Microsofties and Hubbers who I learned from every day.

And of course, shout-out to the Microsoft-internal MCP Security Core Crew - the “deep into everything security” people I’ve been collaborating with in the past year to make sure that we improve the MCP security posture inside Microsoft and outside of it (I think we did a pretty solid job): Barry Dorrans, Alex Sklar, Matthew Henderson, Nazmus Sakib, Stuart Schaefer, Tolga Acar, Diana Smetters, Pam Dingle, and David Parks.

A heartkelp thank you to all of you.
A heartkelp thank you to all of you!

Onward #

It also wouldn’t be a farewell post without a stereotypical “badge on the laptop” photo - I always found the implied tradition somewhat funny, as if we’re cops handing in our badge and gun.

Microsoft badge on a Microsoft laptop.
Microsoft badge on a Microsoft laptop.

If there’s one thing I’ve learned in my career - the tech industry is a ridiculously small space. If we worked or otherwise collaborated together, I am sure that we’ll run into each other again many times in the future. Guaranteed.

Another view of the skybridge between Building 16 and Building 18 in Redmond, WA.
Another view of the skybridge between Building 16 and Building 18 in Redmond, WA.

Will share more on the next adventure soon!