diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
 # Changelog for clickhouse-migrate-cli
 
+## 0.2.1.0 — 2026-11-14
+
+### New Features
+
+- Add per-invocation `handleCommandWithExternalDrain` for embedding
+  applications, preserving the ordinary handler, parser and JSON v1.
+
 ## 0.2.0.0 — 2026-11-08
 
 ### Breaking Changes
diff --git a/clickhouse-migrate-cli.cabal b/clickhouse-migrate-cli.cabal
--- a/clickhouse-migrate-cli.cabal
+++ b/clickhouse-migrate-cli.cabal
@@ -1,6 +1,6 @@
 cabal-version: 3.12
 name: clickhouse-migrate-cli
-version: 0.2.0.0
+version: 0.2.1.0
 synopsis: Reusable administrative CLI for ClickHouse migrations
 description:
   Reusable administrative CLI for ClickHouse migrations.
diff --git a/src/ClickHouseMigrate/CLI/Handler.hs b/src/ClickHouseMigrate/CLI/Handler.hs
--- a/src/ClickHouseMigrate/CLI/Handler.hs
+++ b/src/ClickHouseMigrate/CLI/Handler.hs
@@ -1,5 +1,5 @@
 -- | Dispatch with application-owned configuration, sinks and command policy.
-module ClickHouseMigrate.CLI.Handler (commandEnvironment, handleCommand, planReport, quiescenceForCommand) where
+module ClickHouseMigrate.CLI.Handler (commandEnvironment, handleCommand, handleCommandWithExternalDrain, planReport, quiescenceForCommand) where
 
 import ClickHouseMigrate
 import ClickHouseMigrate.Adoption qualified as A
@@ -84,8 +84,17 @@
 
 -- Offline plan deliberately does not force the environment argument.
 handleCommand :: CommandEnvironment -> MigrationPlan -> Command -> IO CommandResult
-handleCommand _ plan PlanCommand = pure (CommandResult "plan" (Right (PlanReport (planReport plan) [(componentNameText (migrationIdComponent identity), migrationNameText (migrationIdName identity)) | view <- NE.toList (inspectSchedule plan), let identity = migrationId view])))
-handleCommand environment plan command =
+handleCommand = handleCommandUsing Nothing
+
+-- | One-call opt-in for an embedding application that already validated and
+-- retained independent external drain authority. Only repair uses this policy;
+-- the ordinary handler remains strict and every normal safety check is retained.
+handleCommandWithExternalDrain :: Repair.RepairObservationSink -> CommandEnvironment -> MigrationPlan -> Command -> IO CommandResult
+handleCommandWithExternalDrain sink = handleCommandUsing (Just sink)
+
+handleCommandUsing :: Maybe Repair.RepairObservationSink -> CommandEnvironment -> MigrationPlan -> Command -> IO CommandResult
+handleCommandUsing _ _ plan PlanCommand = pure (CommandResult "plan" (Right (PlanReport (planReport plan) [(componentNameText (migrationIdComponent identity), migrationNameText (migrationIdName identity)) | view <- NE.toList (inspectSchedule plan), let identity = migrationId view])))
+handleCommandUsing observation environment plan command =
   let name = commandName command
       reject = pure . CommandResult name . Left . CommandRejected
       finish = pure . CommandResult name
@@ -101,7 +110,7 @@
                 UpCommand (UpInput target input initialization _) -> case makeAudit input of
                   Left failure -> reject failure
                   Right audit -> mutate $ \context -> Run.up context (Run.upOptions audit (fmap (\ledger -> L.authorizeInitialization target ledger audit) initialization)) plan >>= finish . either (Left . RunFailed) (Right . RunReport)
-                RepairCommand operation (RepairInput _ expected attempt selected input quiet receipts) -> case recovery config input quiet receipts of
+                RepairCommand operation (RepairInput _ expected attempt selected input quiet receipts) -> case recovery observation config input quiet receipts of
                   Left failure -> reject failure
                   Right evidence -> case buildRepair operation expected attempt selected evidence of
                     Left failure -> reject failure
@@ -125,11 +134,11 @@
                                 ApplyAdoption -> mutate $ \context -> A.applyAdoption context plan adoption >>= finish . either (Left . RunFailed) (Right . RunReport)
                                 ResumeAdoption -> mutate $ \context -> A.resumeAdoption context plan adoption >>= finish . either (Left . RunFailed) (Right . RunReport)
 
-recovery :: TargetConfig -> AuditInput -> Bool -> [L.AppendReceipt] -> Either CommandError Repair.RecoveryEvidence
-recovery config input quiet receipts = do
+recovery :: Maybe Repair.RepairObservationSink -> TargetConfig -> AuditInput -> Bool -> [L.AppendReceipt] -> Either CommandError Repair.RecoveryEvidence
+recovery observation config input quiet receipts = do
   established <- makeQuiet (configuredTargetKey config) input quiet >>= maybe (Left QuiescenceRequired) Right
   audit <- makeAudit input
-  either (const (Left InvalidRepairInput)) Right (Repair.recoveryEvidence audit established receipts)
+  either (const (Left InvalidRepairInput)) Right (case observation of Nothing -> Repair.recoveryEvidence audit established receipts; Just sink -> Repair.externalDrainRecoveryEvidence audit established receipts sink)
 
 buildRepair :: RepairOperation -> L.LedgerPosition -> L.AttemptId -> Maybe R.StepAttempt -> Repair.RecoveryEvidence -> Either CommandError Repair.RepairRequest
 buildRepair operation expected attempt selected evidence = either (const (Left InvalidRepairInput)) Right $ case (operation, selected) of
