diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,14 @@
 # Changelog for settei-kdl
 
-## 0.1.0.0 — 2026-07-18
+## 0.2.0.0 — 2026-07-19
 
+- Add `renderKdlErrorText` and `renderKdlErrorsText` for stable, operator-readable,
+  secret-safe adapter diagnostics, including related spans when present.
+
+## 0.1.0.0 — 2026-07-19
+
 - Initial experimental release.
 - Add the canonical KDL v2 mapping with exact spans, deterministic cardinality, explicit
   ambiguity errors, and mounted-file Kubernetes annotations.
+- Reject numeric values whose base-10 exponent magnitude exceeds 4096 instead of
+  attempting an unbounded exact conversion at load time.
diff --git a/settei-kdl.cabal b/settei-kdl.cabal
--- a/settei-kdl.cabal
+++ b/settei-kdl.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.8
 name:               settei-kdl
-version:            0.1.0.0
+version:            0.2.0.0
 synopsis:           KDL v2 sources for Settei
 description:
   Translate a canonical KDL v2 document into a provenance-aware Settei
@@ -42,7 +42,8 @@
     , containers    >=0.6.8   && <0.8
     , generic-lens  >=2.2     && <2.4
     , kdl-hs        >=1.1.1   && <1.2
-    , settei        ==0.1.0.0
+    , scientific    >=0.3.7   && <0.4
+    , settei        ==0.2.0.0
     , text          >=2.1     && <2.2
 
 test-suite settei-kdl-tests
@@ -60,8 +61,8 @@
     , base          >=4.21    && <5
     , containers    >=0.6.8   && <0.8
     , generic-lens  >=2.2     && <2.4
-    , settei        ==0.1.0.0
-    , settei-kdl    ==0.1.0.0
+    , settei        ==0.2.0.0
+    , settei-kdl    ==0.2.0.0
     , tasty         >=1.5     && <1.6
     , tasty-hunit   >=0.10.2  && <0.11
     , text          >=2.1     && <2.2
diff --git a/src/Settei/Kdl.hs b/src/Settei/Kdl.hs
--- a/src/Settei/Kdl.hs
+++ b/src/Settei/Kdl.hs
@@ -27,6 +27,8 @@
     kdlSpanEndLine,
     kdlSpanLine,
     readKdlSource,
+    renderKdlErrorText,
+    renderKdlErrorsText,
     withKdlSourcePath,
   )
 where
@@ -38,6 +40,7 @@
 import Data.List.NonEmpty qualified as NonEmpty
 import Data.Map.Strict qualified as Map
 import Data.Maybe (listToMaybe)
+import Data.Scientific (base10Exponent)
 import Data.Text qualified as Text
 import Data.Text.IO qualified as TextIO
 import Data.Text.Read qualified as TextRead
@@ -164,6 +167,49 @@
 kdlSpanEndColumn :: KdlSpan -> Int
 kdlSpanEndColumn value = value ^. #endColumn
 
+-- | Render one KDL input failure as a single operator-readable line.
+--
+-- The line leads with the stable source name, then a parenthetical of the
+-- colon-joined available pieces (path, primary span start line, start column),
+-- then the structural context and fixed message. When a second trustworthy span
+-- exists it is appended as @; also at LINE:COLUMN@. No raw configuration value
+-- can appear because 'KdlSourceError' retains none.
+renderKdlErrorText :: KdlSourceError -> Text
+renderKdlErrorText problem =
+  problem ^. #name
+    <> locationText
+    <> " at "
+    <> problem ^. #context
+    <> ": "
+    <> problem ^. #message
+    <> relatedText
+  where
+    locationText = case locationPieces of
+      [] -> ""
+      pieces -> " (" <> Text.intercalate ":" pieces <> ")"
+    locationPieces =
+      maybe [] (pure . Text.pack) (problem ^. #path)
+        <> maybe [] spanPieces (problem ^. #location)
+    spanPieces spanValue =
+      [ Text.pack (show (spanValue ^. #line)),
+        Text.pack (show (spanValue ^. #column))
+      ]
+    relatedText =
+      maybe
+        ""
+        ( \spanValue ->
+            "; also at "
+              <> Text.pack (show (spanValue ^. #line))
+              <> ":"
+              <> Text.pack (show (spanValue ^. #column))
+        )
+        (problem ^. #relatedLocation)
+
+-- | Render every KDL input failure, one line per problem, matching
+-- 'Settei.Render.renderErrorsText' in shape and trailing newline.
+renderKdlErrorsText :: NonEmpty KdlSourceError -> Text
+renderKdlErrorsText = Text.unlines . fmap renderKdlErrorText . NonEmpty.toList
+
 -- | Decode one KDL v2 document into a Settei file source.
 --
 -- Type annotations, non-finite numbers, duplicate properties, invalid key segments, and
@@ -407,12 +453,33 @@
       Just (child : _) <- [childGroups ^. at name]
     ]
 
+-- | Largest absolute base-10 exponent accepted for a numeric value.
+--
+-- 'toRational' materializes @10 ^ exponent@ as an exact 'Integer', so cost
+-- grows with the exponent's magnitude rather than the input's length. This
+-- adapter-local bound leaves core 'RawNumber' values unbounded.
+maximumScalarExponent :: Int
+maximumScalarExponent = 4096
+
 translateValue :: KdlSourceOptions -> [Text] -> KDL.Value -> Either KdlSourceError RawValue
 translateValue options path value = do
   rejectValueAnnotation options path value
   case valueData value of
     KDL.String text -> Right (RawText text)
-    KDL.Number number -> Right (RawNumber (toRational number))
+    KDL.Number number
+      | scalarExponent < negate maximumScalarExponent || scalarExponent > maximumScalarExponent ->
+          Left
+            ( kdlError
+                options
+                KdlUnsupportedValue
+                (Just (valueLocation value))
+                Nothing
+                path
+                "numeric value exponent is out of the supported range"
+            )
+      | otherwise -> Right (RawNumber (toRational number))
+      where
+        scalarExponent = base10Exponent number
     KDL.Bool boolean -> Right (RawBool boolean)
     KDL.Null -> Right RawNull
     KDL.Inf -> unsupported
diff --git a/test/Settei/KdlCharacterizationTest.hs b/test/Settei/KdlCharacterizationTest.hs
--- a/test/Settei/KdlCharacterizationTest.hs
+++ b/test/Settei/KdlCharacterizationTest.hs
@@ -7,7 +7,7 @@
 import Settei
 import Settei.Kdl
 import Settei.Prelude
-import Test.Tasty (TestTree, testGroup)
+import Test.Tasty (TestTree, localOption, mkTimeout, testGroup)
 import Test.Tasty.HUnit (assertBool, testCase, (@?=))
 
 tests :: TestTree
@@ -56,7 +56,31 @@
         assertBool "syntax error omitted its column" (maybe False ((> 0) . kdlSpanColumn) (kdlErrorSpan problem))
         assertBool
           "syntax error retained the source excerpt"
-          (not (sentinel `Text.isInfixOf` Text.pack (show problem)))
+          (not (sentinel `Text.isInfixOf` Text.pack (show problem))),
+      localOption (mkTimeout 10000000) $
+        testGroup
+          "bounded numeric value conversion"
+          [ testCase "huge positive exponents are rejected quickly" $ do
+              problem <- expectError "huge 1e1000000000\n"
+              expectExponentError "$.huge" problem,
+            testCase "huge negative exponents are rejected quickly" $ do
+              problem <- expectError "tiny 1e-1000000000\n"
+              expectExponentError "$.tiny" problem,
+            testCase "positive boundary exponents convert exactly" $ do
+              input <- expectSource "edge 1e4096\n"
+              expectValue "edge" input (RawNumber (10 ^ (4096 :: Integer) % 1)),
+            testCase "negative boundary exponents convert exactly" $ do
+              input <- expectSource "edge 1e-4096\n"
+              expectValue "edge" input (RawNumber (1 % 10 ^ (4096 :: Integer))),
+            testCase "exponents immediately above the bound are rejected" $ do
+              problem <- expectError "over 1e4097\n"
+              expectExponentError "$.over" problem,
+            testCase "ordinary decimals and hex values remain exact" $ do
+              input <- expectSource "rate 1.5e-3\nmask 0x1A\ncoefficient 12345678901234567890\n"
+              expectValue "rate" input (RawNumber (3 % 2000))
+              expectValue "mask" input (RawNumber 26)
+              expectValue "coefficient" input (RawNumber 12345678901234567890)
+          ]
     ]
 
 sourceOptions :: KdlSourceOptions
@@ -76,6 +100,13 @@
 expectValue keyText input expected = case lookupSource (validKey keyText) input of
   Right (Just found) -> assertBool "unexpected KDL raw value" (candidateValue found == expected)
   _ -> fail "expected KDL candidate"
+
+expectExponentError :: Text -> KdlSourceError -> IO ()
+expectExponentError expectedContext problem = do
+  kdlErrorCategory problem @?= KdlUnsupportedValue
+  fmap kdlSpanLine (kdlErrorSpan problem) @?= Just 1
+  kdlErrorContext problem @?= expectedContext
+  kdlErrorMessage problem @?= "numeric value exponent is out of the supported range"
 
 validKey :: Text -> Key
 validKey value = either (error . show) id (parseKey value)
diff --git a/test/Settei/KdlTest.hs b/test/Settei/KdlTest.hs
--- a/test/Settei/KdlTest.hs
+++ b/test/Settei/KdlTest.hs
@@ -15,7 +15,8 @@
 tests =
   testGroup
     "Settei.Kdl"
-    [ testCase "nested nodes become segmented keys with exact spans" $ do
+    [ errorRenderingTests,
+      testCase "nested nodes become segmented keys with exact spans" $ do
         input <- expectSource "runtime {\n  environment \"production\"\n}\nservice {\n  http {\n    host \"0.0.0.0\"\n    port 8080\n  }\n}\n"
         host <- expectCandidate serviceHttpHost input
         assertBool "host did not remain text" (candidateValue host == RawText "0.0.0.0")
@@ -65,14 +66,13 @@
       testCase "higher KDL overrides leaves and replaces arrays through core" $ do
         low <- expectNamedSource "low" "service { host \"old.internal\"; port 7000; names \"one\" \"two\"; }\n"
         high <- expectNamedSource "high" "service { port 9000; names \"three\" \"four\"; }\n"
-        result <-
-          expectResolution
-            ( resolve
+        let result =
+              resolve
                 defaultResolveOptions
                 [low, high]
                 ((,,) <$> required hostSetting <*> required portSetting <*> required namesSetting)
-            )
-        result ^. #value @?= ("old.internal", 9000, ["three", "four"])
+        value <- expectResolution result
+        value @?= ("old.internal", 9000, ["three", "four"])
         case result ^. #report . #nodes . at servicePort of
           Just node -> do
             node ^. #origin . _Just . #name @?= "high"
@@ -82,7 +82,8 @@
         let reference = kubernetesRef SecretObject (Just "production") "database-config" (Just "config.kdl")
             options = fromKubernetesMountedFile reference sourceOptions
         input <- expectSourceWith options ("database { password \"" <> secretSentinel <> "\"; }\n")
-        result <- expectResolution (resolve defaultResolveOptions [input] (required passwordSetting))
+        let result = resolve defaultResolveOptions [input] (required passwordSetting)
+        _ <- expectResolution result
         let textOutput = renderResolutionText (result ^. #report)
             jsonOutput = renderResolutionJson (result ^. #report)
         assertBool "secret reached text output" (not (secretSentinel `Text.isInfixOf` textOutput))
@@ -106,6 +107,78 @@
         host ^? to candidateOrigin . #location . _Just . #path @?= Just (Text.pack fixturePath)
     ]
 
+errorRenderingTests :: TestTree
+errorRenderingTests =
+  testGroup
+    "error rendering"
+    [ rendererCase
+        "syntax"
+        KdlSyntaxError
+        "service { broken [\n"
+        "application (application.kdl:1:18) at $: invalid KDL v2 syntax",
+      testCase "IO errors render a stable located prefix" $ do
+        result <- readKdlSource (kdlSourceOptions "missing") "test/fixtures/does-not-exist.kdl"
+        case result of
+          Left errors -> do
+            let problem = NonEmpty.head errors
+            kdlErrorCategory problem @?= KdlIoError
+            assertBool
+              "IO renderer omitted its stable prefix"
+              ( "missing (test/fixtures/does-not-exist.kdl) at $: "
+                  `Text.isPrefixOf` renderKdlErrorText problem
+              )
+          Right _ -> fail "expected missing KDL file to fail",
+      rendererCase
+        "duplicate property"
+        KdlDuplicateProperty
+        "service port=8000 port=9000\n"
+        "application (application.kdl:1:19) at $.service.port: duplicate property; also at 1:9",
+      rendererCase
+        "invalid name"
+        KdlInvalidName
+        "\"service.port\" 8080\n"
+        "application (application.kdl:1:1) at $.service.port: KDL names must be non-empty Settei key segments without dots",
+      rendererCase
+        "unsupported annotation"
+        KdlUnsupportedAnnotation
+        "(record)service { port 8080 }\n"
+        "application (application.kdl:1:1) at $.service: KDL node type annotations are not supported",
+      rendererCase
+        "unsupported value"
+        KdlUnsupportedValue
+        "ratio #inf\n"
+        "application (application.kdl:1:7) at $.ratio: non-finite KDL numbers are not supported",
+      rendererCase
+        "mixed node shape"
+        KdlMixedNodeShape
+        "service \"api\" port=8080\n"
+        "application (application.kdl:1:1) at $.service: positional arguments cannot be combined with properties or children",
+      rendererCase
+        "property-child collision"
+        KdlPropertyChildCollision
+        "service port=7000 {\n  port 8000\n}\n"
+        "application (application.kdl:1:9) at $.service.port: a property and child use the same field name; also at 2:3",
+      testCase "a missing path omits it while retaining the primary span" $ do
+        problem <-
+          expectErrorWith
+            (kdlSourceOptions "memory")
+            "service \"api\" port=8080\n"
+        assertBool
+          "position-only KDL renderer omitted its location"
+          ("memory (1:1)" `Text.isPrefixOf` renderKdlErrorText problem),
+      testCase "plural rendering is singular rendering plus a newline" $ do
+        problem <- expectError "service \"api\" port=8080\n"
+        renderKdlErrorsText (NonEmpty.singleton problem)
+          @?= renderKdlErrorText problem <> "\n"
+    ]
+
+rendererCase :: String -> KdlErrorCategory -> Text -> Text -> TestTree
+rendererCase label expectedCategory input expected =
+  testCase label $ do
+    problem <- expectError input
+    kdlErrorCategory problem @?= expectedCategory
+    renderKdlErrorText problem @?= expected
+
 sourceOptions :: KdlSourceOptions
 sourceOptions = withKdlSourcePath "application.kdl" (kdlSourceOptions "application")
 
@@ -122,7 +195,10 @@
   Right sourceValue -> pure sourceValue
 
 expectError :: Text -> IO KdlSourceError
-expectError input = case decodeKdlSource sourceOptions input of
+expectError = expectErrorWith sourceOptions
+
+expectErrorWith :: KdlSourceOptions -> Text -> IO KdlSourceError
+expectErrorWith options input = case decodeKdlSource options input of
   Left errors -> pure (NonEmpty.head errors)
   Right _ -> fail "expected KDL decoding to fail"
 
@@ -136,8 +212,8 @@
   found <- expectCandidate key input
   assertBool "unexpected KDL raw value" (candidateValue found == expected)
 
-expectResolution :: Either (NonEmpty ConfigError) a -> IO a
-expectResolution = \case
+expectResolution :: ResolveResult a -> IO a
+expectResolution result = case result ^. #answer of
   Left errors -> fail (Text.unpack (renderErrorsText errors))
   Right value -> pure value
 
