fix(validation): reject unknown keys in --global-arg and override --input flags#1312
fix(validation): reject unknown keys in --global-arg and override --input flags#1312
Conversation
…nput flags Zod's default strip mode was silently discarding unknown keys passed via `--global-arg` on `model create` and via `--input` on `model method run` (for inputs not matched by the model's inputs schema). This meant typos in flag names gave no feedback and the incorrect key was quietly ignored. Fix: use `.strict()` (via duck-typed cast for Zod v4 compatibility) at all three global-argument validation sites (create, validateModel, executeWorkflow) so unrecognised keys yield a clear error. Unknown override-input keys are now detected in the libswamp run generator before they reach the domain execution layer, which keeps the check out of the workflow execution path where step inputs forwarded as method arguments would cause false positives. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
The core validation change is solid — applying .strict() to reject unknown keys in --global-arg and --input flags is the right fix. The duck-typed cast for Zod v4 compatibility is pragmatic, test coverage is thorough (both rejection and acceptance paths), and the domain layering is correct: schema validation stays in the domain services while the override-input key check lives in the libswamp orchestration layer where it belongs.
No blocking issues.
Suggestions
-
Unrelated gitignore change bundled in (
repo_service.ts): The.claude/→.claude/worktrees/+ specific file paths change is a good improvement (allows committing.claude/skills/), but it's unrelated to the validation fix. Ideally it would be a separate commit/PR for cleaner history and easier bisection. Not a blocker since it's well-tested. -
Multi-line comment in
run.ts:353-357: The 5-line block explaining override input semantics carries important context but could be condensed to 1-2 lines per CLAUDE.md conventions (e.g.,// Reject unknown override inputs before Zod's strip mode silently discards them). The existing phrasing documents the "what" alongside the "why." -
Comment in
method_execution_service_test.ts:352-353: The 2-line comment explaining why the test fixture was changed is useful context, but could be a single line:// Empty args: exercises Zod error path without triggering unknown-key rejection.
There was a problem hiding this comment.
CLI UX Review
Blocking
None.
Suggestions
-
model create --global-argerror has a confusing:prefix (src/libswamp/models/create.ts:172). The issue formatter uses${i.path.join(".")}: ${i.message}. For regular Zod errors (e.g. missing required field), path is["name"]→name: Required. But Zod'sunrecognized_keysissue has an empty path[], producing: Unrecognized key(s) in object: 'typoKey'. The leading:looks like broken output. The formatter pre-dates this PR but this is the first time an empty-path issue is produced here. Could guard withi.path.length > 0 ?${i.path.join(".")}:: ""before the message. -
model create --global-argerror doesn't tell users what valid keys are. Themodel method run --inputerror helpfully addsValid inputs are: key1, key2, but the--global-argerror only says what was wrong, not what's accepted. After seeingUnrecognized key(s) in object: 'typoKey', a user's next question is "ok, what keys are valid?" — they'd have to runswamp model type getto find out. Not a blocker (the fix itself is valuable), but the--inputerror's pattern is worth following.
Verdict
PASS — the validation tightening improves UX by surfacing previously silent failures with clear, actionable errors. Both new error paths (model create and model method run) tell users what went wrong. The .gitignore change is transparent to users.
There was a problem hiding this comment.
Adversarial Review
Critical / High
None found.
Medium
-
src/libswamp/models/run.ts:374-376—inoperator checks prototype chain, allowing prototype property names to bypass the unknown-key check.
The override input validation uses!(k in methodSchema.shape!)to detect unknown keys. Theinoperator walks the prototype chain, so input keys that happen to matchObject.prototypeproperties (constructor,toString,valueOf,hasOwnProperty,__proto__, etc.) would pass this check even though they aren't valid method arguments. They'd then be set as method arguments and silently stripped by Zod's non-strictsafeParsein theexecutepath — the user gets no error for the typo.Breaking example:
swamp model method run my-model run --input constructor=oops— no error is raised; the key is silently discarded.Suggested fix: Replace
!(k in methodSchema.shape!)with!Object.hasOwn(methodSchema.shape!, k)(or!Object.prototype.hasOwnProperty.call(methodSchema.shape!, k)). -
src/libswamp/models/run.ts:370-372and all 4.strict()sites — silent fallback when schema is not a plainZodObject.
If a user extension definesglobalArgumentsor methodargumentsviaz.object({...}).refine(...)(or.transform(),.pipe()), the wrappingZodEffectsdoesn't have.strict()or.shape. Both the.strict()?.() ?? fallbackpattern and the.shape !== undefinedguard silently degrade to the previous strip behavior. This means extensions using refinements don't benefit from the fix.This isn't a regression (they get the same behavior as before the PR), but it is a gap in coverage that may be worth documenting or addressing. The
coerceMethodArgsutility already has anunwrapSchemafunction that unwraps effects/optionals to find the innerZodObject— a similar approach could be used here to reach the inner object's.strict()or.shape.
Low
-
src/domain/models/method_execution_service.ts:432-442—hasUnresolvedbranch ofexecuteWorkflowdoesn't apply strict validation.
When global args contain expression fields (${{ ... }}), the code takes thehasUnresolvedbranch which only coerces resolved fields and sets them directly — nosafeParse()at all. Unknown keys among the resolved subset pass through unchecked. This is pre-existing behavior and not a regression, but it means an unknown key like--global-arg typoKey=oopsalongside an expression-containing field would be silently accepted. -
Unrelated
.gitignorechange bundled with validation fix. Therepo_service.tschange from.claude/to specific file exclusions is a separate concern. It's correctly tested and the specific files chosen (worktrees, settings.local.json, scheduled_tasks.*) make sense for the stated goal of letting extension authors commit.claude/skills/. Just noting it's orthogonal to the validation fix.
Verdict
PASS — The core fix is sound: unknown keys in --global-arg and override --input flags are now rejected at all three validation sites (create, validateModel, executeWorkflow) and in the libswamp run generator. Tests cover the happy path, rejection path, and the workflow-step-inputs non-regression. The .strict() duck-typing pattern is a reasonable Zod v4 compatibility approach. The medium findings are real gaps but not regressions — they represent cases where the previous silent-strip behavior is preserved rather than improved.
Three follow-ups from the #1312 adversarial review: 1. Prototype-chain bypass: the unknown-key check used `k in shape` which walks Object.prototype, so flag keys like `--input constructor=oops` bypassed the check. Switch to `Object.hasOwn(shape, k)`. 2. ZodEffects not unwrapped: schemas defined with `.refine()`, `.transform()`, or `.pipe()` lack `.strict()` and `.shape` on the outer wrapper, so the previous duck-typed cast silently fell back to strip mode. Introduce `getObjectShape()` that uses the existing `unwrapSchema` helper to find the inner ZodObject's shape, then do an explicit unknown-key check against it. The original schema then handles normal validation, preserving refinements/transforms. 3. Zod v3 vs v4 internals: extensions may import `npm:zod@3` (where `_def.typeName` is e.g. `"ZodObject"` and `_def.shape` is a function) or `npm:zod@4` (where `_def.type` is `"object"` and `_def.shape` is a value). Normalize both in `getSchemaType()` and `readShape()`. Also fixes the CLI UX issue from the #1312 review: the `model create` error no longer shows a leading `: ` prefix because the unknown-key error is now produced explicitly with a clear message naming valid arguments, instead of via Zod's empty-path `unrecognized_keys` issue. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
--global-argonmodel create(and in YAML definitions) are now rejected with a clear error instead of being silently stripped by Zod's default mode--inputonmodel method run(keys not in the model'sinputs.properties) are validated against the method schema and rejected with a clear errorcreate,validateModel,executeWorkflow) use.strict()via a duck-typed cast for Zod v4 compatibility where.strict()only exists onZodObjectTest plan
create_test.ts: unknown global arg key yieldsvalidation_failederrorvalidation_service_test.ts:validateModelrejects unknown global arg and method arg keysrun_test.ts: unknown override input key yieldsvalidation_failederror; known key succeedsmethod_execution_service_test.ts:executeWorkflowrejects unknown global arg keyinputs_test.ts,vary_test.ts,workflow_partial_inputs_test.ts: workflow step inputs (CEL expression variables) forwarded as method arguments no longer trigger false positivesswamp model create test-strict bad-name --global-arg name=hello --global-arg typoKey=oopsnow errors; before this fix it silently succeededFixes swamp-club#244
🤖 Generated with Claude Code