Follow-up to #35470.
The assertion added in #35470 to prevent duplicate option registration across categories was too strict, it also fired when an option was registered in OptionsCategory::HIDDEN and then again in a real category (or vice versa).
That is intentional behavior introduced in #13441: options unavailable in a given binary (e.g. GUI args in bitcoind) are pre-registered as hidden so shared bitcoin.conf files don't fail. In bitcoin-qt, SetupServerArgs registers GUI args as hidden, then SetupUIArgs registers them properly under OptionsCategory::GUI, triggering the assertion and crashing on startup (e.g. bitcoin-qt crashes now that #35470 has been merged into master).
The fix relaxes the assertion to exclude HIDDEN from the cross-category duplicate check, preserving the original intent of #13441 while still catching unintentional duplicates between real categories.
<details> <summary>Alternative approach considered</summary>
An alternative fix would have been to make AddHiddenArgs skip args already registered in any category:
void ArgsManager::AddHiddenArgs(const std::vector<std::string>& names)
{
for (const std::string& name : names) {
size_t eq_index = name.find('=');
std::string arg_name = name.substr(0, eq_index == std::string::npos ? name.size() : eq_index);
LOCK(cs_args);
bool already_registered = std::ranges::any_of(m_available_args, [&](const auto& arg_map) {
return arg_map.second.contains(arg_name);
});
if (!already_registered) {
AddArg(name, "", ArgsManager::ALLOW_ANY, OptionsCategory::HIDDEN);
}
}
}
This would fix the crash but obscures the intent — silently skipping registrations in AddHiddenArgs makes it harder to reason about what's registered. The chosen approach of relaxing the assertion specifically for HIDDEN is more explicit about why the duplicate is allowed.
</details>