diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+# 0.9.2.0
+
+* Bug fix: release `Reader` Vault keys when their handler scope exits.
+
+* Make `Vault`'s `Key` type role representational.  This is
+  technically a PVP violation but since it's fixing a type safety bug
+  we're not going to release a major version for it.
+
 # 0.9.1.0
 
 * Add `yieldToPureList`
diff --git a/bluefin-internal.cabal b/bluefin-internal.cabal
--- a/bluefin-internal.cabal
+++ b/bluefin-internal.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.0
 name:               bluefin-internal
-version:            0.9.1.0
+version:            0.9.2.0
 license:            MIT
 license-file:       LICENSE
 author:             Tom Ellis
diff --git a/src/Bluefin/Internal.hs b/src/Bluefin/Internal.hs
--- a/src/Bluefin/Internal.hs
+++ b/src/Bluefin/Internal.hs
@@ -22,8 +22,8 @@
     oneWayCoercion,
     trans3D,
     unsafeCoercionOfOneWayCoercion,
-    unsafeOneWayCoercible,
     unsafeOneWayCoerce,
+    unsafeOneWayCoercible,
   )
 import Bluefin.Internal.Vault (Vault)
 import Bluefin.Internal.Vault qualified as Vault
@@ -1656,11 +1656,14 @@
   (forall e. Reader r e -> Eff (e :& es) a) ->
   Eff es a
 runReader r f = do
-  k <- UnsafeMkEff $ \vault -> do
-    k <- Vault.newKey
-    modifyIORef' vault (\v -> Vault.insert k r v)
-    pure k
-  makeOp (f (MkReader k))
+  bracket
+    ( UnsafeMkEff $ \vault -> do
+        k <- Vault.newKey
+        modifyIORef' vault (\v -> Vault.insert k r v)
+        pure k
+    )
+    (\k -> UnsafeMkEff $ \vault -> modifyIORef' vault (Vault.delete k))
+    (\k -> makeOp (f (MkReader k)))
 
 -- | Read the value.  Note that @ask@ has the property that these two
 -- operations are always equivalent:
diff --git a/src/Bluefin/Internal/CloneableHandle.hs b/src/Bluefin/Internal/CloneableHandle.hs
--- a/src/Bluefin/Internal/CloneableHandle.hs
+++ b/src/Bluefin/Internal/CloneableHandle.hs
@@ -117,10 +117,10 @@
 
 hcHandleReader :: (CloneableHandle h) => HandleCloner (HandleReader h) (HandleReader h) e
 hcHandleReader = MkHandleCloner $ \hr k -> do
-  h <- askHandle hr
-  cloneHandleClass h $ \h' -> do
-    runHandleReader h' $ \hr' -> do
-      useImplIn k (mapHandle hr')
+  asksHandle hr $ \h -> do
+    cloneHandleClass h $ \h' -> do
+      runHandleReader h' $ \hr' -> do
+        useImplIn k (mapHandle hr')
 
 instance
   (TypeError (Text "Coroutine cannot be cloned. Perhaps you want an STM channel?")) =>
diff --git a/src/Bluefin/Internal/Vault.hs b/src/Bluefin/Internal/Vault.hs
--- a/src/Bluefin/Internal/Vault.hs
+++ b/src/Bluefin/Internal/Vault.hs
@@ -1,3 +1,5 @@
+{-# LANGUAGE RoleAnnotations #-}
+
 module Bluefin.Internal.Vault
   ( module Bluefin.Internal.Vault,
     Vault,
@@ -18,6 +20,8 @@
 type Key :: Type -> Type
 newtype Key a = MkKey (Vault.Key Any)
 
+type role Key representational
+
 fromMine :: Key a -> Vault.Key a
 fromMine (MkKey k) = unsafeCoerce k
 
@@ -35,3 +39,6 @@
 
 insert :: Key a -> a -> Vault -> Vault
 insert = Vault.insert . fromMine
+
+delete :: Key a -> Vault -> Vault
+delete = Vault.delete . fromMine
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -4,8 +4,11 @@
 module Main (main) where
 
 import Bluefin.Internal
+import Bluefin.Internal.Vault qualified as Vault
 import Control.Monad (forever, when)
 import Data.Foldable (for_)
+import Data.IORef (readIORef)
+import Data.Maybe (isNothing)
 import Test.GeneralBracket (test_generalBracket)
 import Test.SpecH (SpecH, assertEqual, runSpecH)
 import Prelude hiding (break, read)
@@ -44,6 +47,7 @@
       ([20, 30], "Hello")
 
     test_localInHandler y
+    test_readerCleanup y
     test_generalBracket io y
     test_streamConsumeReader y
     test_streamConsumeHandleReader y
@@ -94,6 +98,26 @@
   for_ as (yield y)
   pure r
 
+test_readerCleanup :: (e <: es) => SpecH e -> Eff es ()
+test_readerCleanup y = runReader @Int 1 $ \outer -> do
+  for_ [False, True] $ \abort -> do
+    key <- withEarlyReturn $ \ex ->
+      runReader @Int 2 $ \(MkReader key) ->
+        if abort then returnEarly ex key else pure key
+    do
+      -- Use the internals to check a property that cannot be tested without them.
+      cleanedUp <- UnsafeMkEff $ \vault -> do
+        contents <- readIORef vault
+        pure (isNothing (Vault.lookup key contents))
+      assertEqual
+        y
+        "Reader key released on normal and exceptional exit"
+        True
+        cleanedUp
+    do
+      outerValue <- ask outer
+      assertEqual y "Outer Reader survives inner cleanup" 1 outerValue
+
 test_localInHandler :: (e <: es) => SpecH e -> Eff es ()
 test_localInHandler y = runReader "global" $ \re ->
   forEach
@@ -143,7 +167,7 @@
       )
 
 -- localHandle run in one branch of a streamConsume should not affect
--- askHandle in the other branch.
+-- asksHandle in the other branch.
 test_streamConsumeHandleReader :: (e <: es) => SpecH e -> Eff es ()
 test_streamConsumeHandleReader spech = do
   runConstEffect @Int 0 $ \ce ->
@@ -152,8 +176,8 @@
         ( \y -> do
             let s = yield y ()
             let check i = do
-                  MkConstEffect i' <- askHandle r
-                  assertEqual spech "HandleReader local" i i'
+                  asksHandle r $ \(MkConstEffect i') -> do
+                    assertEqual spech "HandleReader local" i i'
             check 0
             s
             check 0
