Negative Constraints in Prompts: Banned APIs That Safer Codegen Respects

Negative Constraints in Prompts: Banned APIs That Safer Codegen Respects

Seniors already ban child_process.exec with unsanitized input, aws-sdk v2 in new code, and hard-coded credentials—then the coding agent cheerfully regenerates them. Negative constraints encode those bans in versioned system prompts and in machine-checkable validators so codegen fails closed when it ignores the list.

⚡ TL;DR: Maintain a banned_apis.yaml sourced from lint rules; inject a compressed NEGATIVES block into system prompts; validate model patches with AST/grep gates before apply; version with Prompt Management. Pair with LLM output validators and Bedrock Guardrails.

Single source of truth

# banned_apis.yaml — generated from eslint/deny + security ADRs
version: 17
banned:
  - id: no-exec-string
    match: { module: "child_process", exports: ["exec", "execSync"] }
    reason: "Use execFile with arg arrays; see ADR-0142"
  - id: no-aws-sdk-v2
    match: { module: "aws-sdk" }
    reason: "Use @aws-sdk/* v3 clients"
  - id: no-hardcoded-secrets
    match: { regex: "(AKIA[0-9A-Z]{16}|secretAccessKey\\s*[:=])" }
    reason: "Secrets belong in SSM/Secrets Manager"
  - id: no-verifalse-tls
    match: { regex: "rejectUnauthorized\\s*:\\s*false" }
    reason: "Never disable TLS verification"
deprecated_modules:
  - "request"
  - "moment"

CI publishes a compact prompt fragment:

## NEGATIVES (must not generate)
- Do NOT import aws-sdk (v2). Use @aws-sdk/* v3.
- Do NOT call child_process.exec/execSync with string commands.
- Do NOT disable TLS (rejectUnauthorized:false).
- Do NOT embed AKIA… keys or secretAccessKey literals.
If a task seems to require a banned API, explain and propose the approved alternative.

Prompt + validator sandwich

import banned from "./banned_apis.json";

function systemPrompt(task: string): string {
  return [
    BASE_CODER_PROMPT,
    renderNegatives(banned), // versioned fragment
    `Task:\n${task}`,
  ].join("\n\n");
}

function assertNoBanned(patch: string): void {
  for (const rule of banned.banned) {
    if (rule.match.module && importUses(patch, rule.match)) {
      throw new Error(`banned:${rule.id}:${rule.reason}`);
    }
    if (rule.match.regex && new RegExp(rule.match.regex).test(patch)) {
      throw new Error(`banned:${rule.id}:${rule.reason}`);
    }
  }
}
async function safeApply(task: string) {
  const patch = await generatePatch(systemPrompt(task));
  try {
    assertNoBanned(patch);
  } catch (e) {
    // One repair loop with the violation named
    const repaired = await generatePatch(
      systemPrompt(task) + `\nREPAIR: ${String(e)}. Use approved alternatives only.`,
    );
    assertNoBanned(repaired); // fail closed on second violation
    return applyPatch(repaired);
  }
  return applyPatch(patch);
}

Do not rely on prompt-only bans without AST/grep gates — models drift; lint is the backstop.

Keep negatives short and current

Practice Why
Cap NEGATIVES block ~40–60 lines Long lists get ignored
Generate from lint deny lists Avoid prompt/lint drift
Version + changelog Know what prod agents saw
Prefer “use X instead” Models need alternatives

Rotate deprecated items into the list when eslint bans land; remove entries once the codebase cannot import them anymore (noise reduction).

Eval the negatives

# canary: agent must refuse or rewrite banned patterns
cases = load_yaml("evals/banned_api_tasks.yaml")
for c in cases:
    patch = run_agent(c["task"])
    assert not violates(patch, banned), c["id"]
    assert c["approved_alternative_marker"] in patch

Wire into CI canaries so prompt edits that drop NEGATIVES fail loud.

Closing checklist

Dos
– Generate NEGATIVES from the same source as lint deny rules
– Inject a short versioned fragment into system prompts
– Validate patches with AST/regex gates; one repair then fail closed
– Offer approved alternatives in the prompt text
– Canary banned-API tasks on every prompt release

Donts
– Do not rely on prose bans alone
– Do not paste a 500-line policy into every prompt
– Do not let prompt and eslint diverge
– Do not silently strip banned lines without telling the user
– Do not forget secret regexes (keys, tokens) in the same list

Related reading

Last updated on September 11, 2026


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply