diff --git a/crates/engine/src/cmd/modify/transform.rs b/crates/engine/src/cmd/modify/transform.rs
--- a/crates/engine/src/cmd/modify/transform.rs
+++ b/crates/engine/src/cmd/modify/transform.rs
@@ -66,7 +66,10 @@ pub(super) fn run_move(s: &mut Session, p: &Value) -> Result<Value> {
 pub(super) fn run_copy(s: &mut Session, p: &Value) -> Result<Value> {
     let hs = targets(s, p)?;
     let d = delta_of("copy", p)?;
-    let n = p.get("count").and_then(Value::as_u64).unwrap_or(1).clamp(1, 10_000);
+    let n = match p.get("count") {
+        None => 1,
+        Some(v) => v.as_u64().filter(|n| (1..=10_000).contains(n)).ok_or_else(|| bad("copy", "`count` must be an integer from 1 to 10000"))?,
+    };
     let mut out = Vec::new();
     for k in 1..=n {
         out.extend(transform_entities(s, &hs, &Mat3::translate(d * k as f64), true)?.into_iter().map(|h| h.hex()));
diff --git a/crates/engine/tests/it/copy_count.rs b/crates/engine/tests/it/copy_count.rs
new file mode 100644
--- /dev/null
+++ b/crates/engine/tests/it/copy_count.rs
@@ -0,0 +1,24 @@
+//! JSON COPY refuses a `count` that is not an integer from 1 to 10000 instead of clamping it.
+
+use cadcraft_engine::Session;
+use serde_json::{Value, json};
+
+fn entities(s: &Session) -> usize {
+    s.doc().map(|d| d.model.iter().count()).unwrap_or(0)
+}
+
+#[test]
+fn invalid_copy_count_is_refused() {
+    let mut s = Session::new();
+    let r = s.execute("line", &json!({"points": [[0, 0], [10, 0]]})).unwrap();
+    let h = r["handles"][0].clone();
+    let before = entities(&s);
+    let bad: [Value; 7] = [json!(0), json!(-3), json!(2.5), json!("abc"), json!(null), json!(10001), json!(50000)];
+    for c in bad {
+        let r = s.execute("copy", &json!({"handles": [h], "delta": [0, 5], "count": c}));
+        assert!(r.is_err(), "count {c} should be refused");
+        assert_eq!(entities(&s), before, "count {c} changed the drawing");
+    }
+    s.execute("copy", &json!({"handles": [h], "delta": [0, 5], "count": 3})).unwrap();
+    assert_eq!(entities(&s), before + 3);
+}
diff --git a/crates/engine/tests/it/main.rs b/crates/engine/tests/it/main.rs
--- a/crates/engine/tests/it/main.rs
+++ b/crates/engine/tests/it/main.rs
@@ -20,6 +20,7 @@ mod burst;
 mod compare_export;
 mod constraint_geometry;
 mod constraint_infer;
+mod copy_count;
 mod count;
 mod dash_block;
 mod data_extraction;
