Install
$ agentstack add skill-jagreehal-jagreehal-claude-skills-validation-boundary ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Validation at the Boundary
Overview
Validation is a boundary concern. You check passports once at the border, not at every street corner. Untrusted input (HTTP bodies, query params, CLI args, queue messages, env vars, third-party responses) is parsed and rejected at the edge of the system. Everything inside the boundary trusts its types by contract.
External Input (HTTP, CLI, Queue, 3rd-party) ;
2. Use Branded Types for Stronger Guarantees
const EmailSchema = z.string().email().brand();
const UserIdSchema = z.string().uuid().brand();
type Email = z.infer; // string & { __brand: 'Email' }
type UserId = z.infer; // string & { __brand: 'UserId' }
// Now TypeScript prevents accidental raw strings
function sendEmail(to: Email, subject: string) { ... }
sendEmail("alice@example.com", "Hello"); // ERROR: string not assignable to Email
sendEmail(EmailSchema.parse("alice@example.com"), "Hello"); // OK
When to Use Branded Types vs Plain Types
| Use Branded Types | Use Plain Types | |-------------------|-----------------| | IDs that look alike (userId, orderId) | Internal-only types | | Security-sensitive values (tokens, keys) | Simple strings with no confusion risk | | Values that MUST go through validation | Prototyping / early development | | Cross-boundary data | Types only used in one function |
Rule of thumb: If mixing up two string parameters would cause a bug, brand them.
3. Validate at HTTP/Queue/CLI Boundaries
app.post('/users', async (req, res) => {
// 1. Validate at the boundary
const parsed = CreateUserSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json(formatZodError(parsed.error));
}
// 2. Call business function with valid, typed data
const user = await userService.createUser(parsed.data);
return res.status(201).json(user);
});
4. Business Functions Trust the Contract
NO validation inside business functions. They trust args are already valid:
// CORRECT - No validation, trust the contract
async function createUser(
args: CreateUserInput, // Already validated!
deps: CreateUserDeps
): Promise {
const user = { id: crypto.randomUUID(), ...args };
await deps.db.saveUser(user);
return user;
}
// WRONG - Validation mixed with business logic
async function createUser(args: { name: string; email: string }, deps) {
if (!args.name || args.name.length ;
};
function formatZodError(error: z.ZodError): ValidationErrorResponse {
return {
error: 'VALIDATION_FAILED',
message: 'Request validation failed',
issues: error.issues.map(issue => ({
path: issue.path.join('.'),
message: issue.message,
code: issue.code,
})),
};
}
Two Layers of Validation
| Type | Where | What | Tool | |------|-------|------|------| | Schema Validation | Boundary | Shape, types, format, ranges | Zod | | Domain Validation | Business function | Business rules (email exists, has permission) | Database lookups |
// Schema validation (boundary)
const TransferSchema = z.object({
fromAccount: z.string().uuid(),
toAccount: z.string().uuid(),
amount: z.number().positive(),
});
// Domain validation (business function)
async function validateTransfer(args: TransferInput, deps: TransferDeps) {
const account = await deps.db.getAccount(args.fromAccount);
if (account.balance { page: 2, limit: 50 }
Partial Updates (PATCH)
const UpdateUserSchema = z.object({
name: z.string().min(2).optional(),
email: z.string().email().optional(),
});
Transforms
const CreatePostSchema = z.object({
title: z.string().transform(s => s.trim()),
slug: z.string().transform(s => s.toLowerCase().replace(/\s+/g, '-')),
});
Express Middleware
function validateBody(schema: z.ZodSchema) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json(formatZodError(result.error));
}
req.body = result.data;
next();
};
}
app.post('/users', validateBody(CreateUserSchema), async (req, res) => {
const user = await userService.createUser(req.body);
res.status(201).json(user);
});
Quick Reference
| Question | Answer | |----------|--------| | Where validate shape/format? | Boundary (Zod schema) | | Where validate business rules? | Business function | | Should fn(args, deps) validate args? | NO. Trust the contract | | Error for invalid input? | HTTP 400 (client error) |
Common Rationalizations
| Rationalization | Reality | |---|---| | "I'll just check the input inside the function too, to be safe" | Double validation means neither layer is authoritative and the function now has two jobs. Parse once at the boundary; trust the type after. | | "It came from our own database, but I'll validate it anyway" | Data that already crossed a boundary (or originated internally) is trusted. Re-validating it is noise that hides where the real boundary is. | | "The third-party API always returns the right shape" | External services are untrusted. They change, fail, and can return malicious or instruction-like content. Parse their responses like any other boundary input. | | "A plain string is fine for the user ID" | If two same-typed values can be swapped by mistake (userId vs orderId), brand them so the compiler catches the mix-up. | | "Throwing inside the business function is simpler than returning a Result for the bad-balance case" | Schema validation belongs at the boundary; business-rule failures (insufficient funds, not found) are expected outcomes: return them as result-types, don't throw. |
Red Flags
if (!args.email)or.length r.json())result used without parsing its shape- Raw
stringparameters for IDs, tokens, or other easily-confused values - A Zod schema defined but only used for types, never
.parse()d at the edge - Validation logic duplicated across multiple handlers instead of shared middleware/schema
Verification
After wiring up input handling:
- [ ] Every external input is parsed with a Zod schema at the boundary
- [ ] Business functions accept already-validated types and contain no shape checks
- [ ] IDs/tokens that could be confused use branded types
- [ ] Third-party responses are parsed before use
- [ ] Invalid input returns a consistent error (HTTP 400 /
VALIDATION_FAILED) - [ ] Business-rule failures are returned as Results, not thrown (see [
result-types](../result-types/SKILL.md)) - [ ] No re-validation of internal or database-sourced data
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: jagreehal
- Source: jagreehal/jagreehal-claude-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.