diff --git a/core/src/main/java/google/registry/tools/DeleteFeatureFlagCommand.java b/core/src/main/java/google/registry/tools/DeleteFeatureFlagCommand.java
new file mode 100644
index 000000000..964678ecf
--- /dev/null
+++ b/core/src/main/java/google/registry/tools/DeleteFeatureFlagCommand.java
@@ -0,0 +1,84 @@
+// Copyright 2025 The Nomulus Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package google.registry.tools;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
+
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.Parameters;
+import google.registry.model.common.FeatureFlag;
+import java.util.List;
+
+/**
+ * Command to remove a {@link FeatureFlag} from the database entirely.
+ *
+ *
This should be used when a flag has been deprecated entirely, and we want to remove it, to
+ * avoid having old invalid data in the database.
+ *
+ *
This command uses the native query format so that it is able to delete values that are no
+ * longer part of the {@link FeatureFlag} enum.
+ *
+ *
This uses {@link ConfirmingCommand} instead of {@link MutatingCommand} because of the
+ * nonstandard deletion flow required by the fact that the enum constant may already have been
+ * removed.
+ */
+@Parameters(separators = " =", commandDescription = "Delete a FeatureFlag from the database")
+public class DeleteFeatureFlagCommand extends ConfirmingCommand {
+
+ @Parameter(description = "Feature flag to delete", required = true)
+ private List mainParameters;
+
+ @Override
+ protected boolean checkExecutionState() {
+ checkArgument(
+ mainParameters != null && !mainParameters.isEmpty() && !mainParameters.getFirst().isBlank(),
+ "Must provide a non-blank feature flag as the main parameter");
+ boolean exists =
+ tm().transact(
+ () ->
+ (long)
+ tm().getEntityManager()
+ .createNativeQuery(
+ "SELECT COUNT(*) FROM \"FeatureFlag\" WHERE feature_name ="
+ + " :featureName",
+ long.class)
+ .setParameter("featureName", mainParameters.getFirst())
+ .getSingleResult()
+ > 0);
+ if (!exists) {
+ System.out.printf("No flag found with name '%s'", mainParameters.getFirst());
+ }
+ return exists;
+ }
+
+ @Override
+ protected String prompt() throws Exception {
+ return String.format("Delete feature flag named '%s'?", mainParameters.getFirst());
+ }
+
+ @Override
+ protected String execute() throws Exception {
+ String featureName = mainParameters.getFirst();
+ tm().transact(
+ () ->
+ tm().getEntityManager()
+ .createNativeQuery(
+ "DELETE FROM \"FeatureFlag\" WHERE feature_name = :featureName")
+ .setParameter("featureName", featureName)
+ .executeUpdate());
+ return String.format("Deleted feature flag with name '%s'", featureName);
+ }
+}
diff --git a/core/src/main/java/google/registry/tools/RegistryTool.java b/core/src/main/java/google/registry/tools/RegistryTool.java
index 869e9a927..d5b08e8b4 100644
--- a/core/src/main/java/google/registry/tools/RegistryTool.java
+++ b/core/src/main/java/google/registry/tools/RegistryTool.java
@@ -54,6 +54,7 @@ public final class RegistryTool {
.put("curl", CurlCommand.class)
.put("delete_allocation_tokens", DeleteAllocationTokensCommand.class)
.put("delete_domain", DeleteDomainCommand.class)
+ .put("delete_feature_flag", DeleteFeatureFlagCommand.class)
.put("delete_host", DeleteHostCommand.class)
.put("delete_premium_list", DeletePremiumListCommand.class)
.put("delete_reserved_list", DeleteReservedListCommand.class)
diff --git a/core/src/test/java/google/registry/tools/DeleteFeatureFlagCommandTest.java b/core/src/test/java/google/registry/tools/DeleteFeatureFlagCommandTest.java
new file mode 100644
index 000000000..f07746466
--- /dev/null
+++ b/core/src/test/java/google/registry/tools/DeleteFeatureFlagCommandTest.java
@@ -0,0 +1,79 @@
+// Copyright 2025 The Nomulus Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package google.registry.tools;
+
+import static com.google.common.truth.Truth.assertThat;
+import static google.registry.model.common.FeatureFlag.FeatureName.TEST_FEATURE;
+import static google.registry.model.common.FeatureFlag.FeatureStatus.ACTIVE;
+import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
+import static google.registry.testing.DatabaseHelper.persistResource;
+import static google.registry.util.DateTimeUtils.START_OF_TIME;
+
+import com.google.common.collect.ImmutableSortedMap;
+import google.registry.model.common.FeatureFlag;
+import org.junit.jupiter.api.Test;
+
+/** Tests for {@link DeleteFeatureFlagCommand}. */
+public class DeleteFeatureFlagCommandTest extends CommandTestCase {
+
+ @Test
+ void testSimpleSuccess() throws Exception {
+ persistResource(
+ new FeatureFlag()
+ .asBuilder()
+ .setFeatureName(TEST_FEATURE)
+ .setStatusMap(ImmutableSortedMap.of(START_OF_TIME, ACTIVE))
+ .build());
+ assertThat(tm().transact(() -> FeatureFlag.isActiveNow(TEST_FEATURE))).isTrue();
+ runCommandForced("TEST_FEATURE");
+ assertThat(FeatureFlag.getUncached(TEST_FEATURE)).isEmpty();
+ }
+
+ @Test
+ void testSuccess_noLongerPartOfEnum() throws Exception {
+ tm().transact(
+ () ->
+ tm().getEntityManager()
+ .createNativeQuery(
+ "INSERT INTO \"FeatureFlag\" VALUES('nonexistent',"
+ + " '\"1970-01-01T00:00:00.000Z\"=>\"INACTIVE\"')")
+ .executeUpdate());
+ assertThat(
+ tm().transact(
+ () ->
+ tm().query(
+ "SELECT COUNT(*) FROM FeatureFlag WHERE featureName ="
+ + " 'nonexistent'",
+ long.class)
+ .getSingleResult()))
+ .isEqualTo(1L);
+ runCommandForced("nonexistent");
+ assertThat(
+ tm().transact(
+ () ->
+ tm().query(
+ "SELECT COUNT(*) FROM FeatureFlag WHERE featureName ="
+ + " 'nonexistent'",
+ long.class)
+ .getSingleResult()))
+ .isEqualTo(0L);
+ }
+
+ @Test
+ void testFailure_nonExistent() throws Exception {
+ runCommandForced("nonexistent");
+ assertInStdout("No flag found with name 'nonexistent'");
+ }
+}