DEV Community

Cover image for 5 Mistakes That Make a Hosted Code Interpreter Lie to Your Users
MICHAEL-MAURICE
MICHAEL-MAURICE

Posted on

5 Mistakes That Make a Hosted Code Interpreter Lie to Your Users

Demo day. Someone drops HostedCodeInterpreterTool into ChatOptions.Tools, uploads a CSV, and the model returns a clean summary with a chart. Applause. Then staging: the same tool list, fluent prose, zero CodeInterpreterToolCallContent. Another run ships a .exe-shaped artifact "so the user can see what was blocked." A third treats a hosted file id the model never downloaded as grounded evidence.

The marker did its job. Policy did not.

HostedCodeInterpreterTool in Microsoft.Extensions.AI is a marker (name code_interpreter). Capable backends may run a sandbox. The class does not enforce MIME allowlists, byte caps, secret shapes, or hash binding. Prefer the Responses path (GetResponsesClient().AsIChatClient() or equivalent) so CodeInterpreterToolCallContent / CodeInterpreterToolResultContent surface cleanly.

This post is five mistakes I keep seeing — idea, why it hurts, skeleton of the fix. Snippets are truncated on purpose.

Tech Skill Builder is $5/month for a very limited time only (normally $10). Grab it now before it goes back to $10. The complete, working, tested project behind this post (hosted-code-interpreter-dotnet) is inside — full source, offline fakes, and an in-depth PDF. Join Tech Skill Builder

Mistake 1: Treating "tool registered" as "interpreter ran"

Marker present ≠ policy. Tool registered ≠ interpreter ran. If the assistant answers a computational question with zero CodeInterpreterToolCallContent (and no matching result content), that text is parametric memory in a lab coat. Refuse it.

var ran = response.Messages
    .SelectMany(m => m.Contents)
    .Any(c => c is CodeInterpreterToolCallContent
           or CodeInterpreterToolResultContent);

if (!ran)
{
    // AnswerMode.Refused — do not return the model text as grounded.
    // (full GuardedCodeInterpreterClient + audit in the complete project)
}
Enter fullscreen mode Exit fullscreen mode

Your UI should expose InterpreterRan separately from Mode. Ops alerts on "tool in list, zero rounds." CI can use an offline fake that throws if the marker disappears from ChatOptions.

Mistake 2: Stuffing arbitrary uploads into HostedCodeInterpreterTool.Inputs

Inputs is not a dumpster. Without an InputPolicy you will eventually forward secret-shaped blobs, denied MIME types, or executable-looking names into the sandbox.

Skeleton of what policy should check before the factory builds the marker:

public IList<AIContent> FilterAccepted(IEnumerable<AIContent>? inputs)
{
    // Per-input + total byte caps (e.g. 2 MiB / 8 MiB).
    // MIME allowlist (csv/json/png/pdf/…) and hard deny (exe/elf/js/bat…).
    // Fail closed on unknown MIME.
    // Secret shapes: sk-…, ghp_…, AKIA…, BEGIN PRIVATE KEY, xox…
    // Executable-looking names: .exe .dll .so .dylib .bat .cmd .ps1 .sh
    // Unsupported AIContent kinds → reject.
    // full InputPolicy in the complete project
}
Enter fullscreen mode Exit fullscreen mode

Wire it through a factory so nobody "temporarily" assigns raw uploads:

var tool = HostedCodeInterpreterToolFactory.CreateMarker(new()
{
    Inputs = userUploads,
    InputPolicy = new InputPolicy(), // filters before tool.Inputs =
});
// ChatOptions.Tools = [tool]; prefer GetResponsesClient().AsIChatClient()
Enter fullscreen mode Exit fullscreen mode

Mistake 3: Trusting stdout and files from the sandbox

Sandbox output is untrusted data. Cap stdout/stderr bytes. Allowlist artifact MIME. Sniff MZ (0x4D 0x5A) and ELF (0x7F 'E') even when MIME looks friendly. Tripwire injection markers (ignore previous, you are now, role delimiters). Fence survivors in an untrusted block so the next model turn cannot treat them as instructions.

var (decisions, totalBytes, fencedOut, fencedErr) =
    quarantine.QuarantineRound(callId, result.Outputs);

// Per-artifact: Oversized | DisallowedMime | ExecutableRejected
//               | UnfencedHostile | BudgetExceeded | Accepted
// Fence: <untrusted_interpreter_output label="stdout"> … </…>
// full OutputQuarantine + PromptFence in the complete project
Enter fullscreen mode Exit fullscreen mode

Showing a blocked executable "for transparency" reopens the surface you closed. Log the verdict. Render survivors only.

Mistake 4: Treating HashUnbound hosted file ids as grounded

Provider-hosted file ids arrive without bytes in your process. Until you download and hash them, the model can summarize a file you never observed. That verdict is HashUnbound — not Accepted.

// HostedFileContent → ArtifactVerdict.HashUnbound until binder runs
binder.BindHostedFile(fileId, downloadedBytes, mediaType, name);
decision = binder.ReclassifyHosted(decision); // Accepted only if bound

// ModelClaimVerifier: claims about unbound ids → ClaimMismatch / refuse
// full ArtifactHashBinder in the complete project
Enter fullscreen mode Exit fullscreen mode

Grounded means you hold a SHA-256 of bytes your app actually saw. A file id string is a pointer, not evidence.

Mistake 5: No budget, wrong mode, "helpful" blocked artifacts

Without a run budget the interpreter can chew rounds, wall clock, and output bytes until your bill or latency SLA breaks. Collapse every answer into a mode monitors can alert on:

Mode Rule of thumb
GroundedOnArtifacts Interpreter ran, ≥1 accepted artifact, zero blocked
Partial Mix of accepted and blocked
Refused Never ran, or nothing usable left (incl. budget exhaustion with zero accepted)
var budget = new RunBudget(new RunBudgetOptions
{
    MaxInterpreterRounds = 3,
    MaxOutputBytes = 4 * 1024 * 1024,
    WallClock = TimeSpan.FromSeconds(45),
});

// ModeFor(accepted, blocked, ran, budgetRefusal)
// Never attach ExecutableRejected payloads to the user-visible answer.
// HTTP can stay 200 on refuse; 500s train clients to retry worse.
// full RunBudget + ModeFor + metrics in the complete project
Enter fullscreen mode Exit fullscreen mode

Partial is honest: keep survivors, drop junk, say so. Refused is honest when the marker never fired. Lying is returning green UI chrome over unbound hashes or blocked binaries.

Ship checklist (short)

  1. Detect CodeInterpreterToolCallContent / result content — refuse computational answers with zero signals.
  2. InputPolicy on every path into HostedCodeInterpreterTool.Inputs.
  3. OutputQuarantine: byte caps, MIME allowlist, MZ/ELF sniff, injection tripwire, prompt fence.
  4. Bind hosted file ids before any claim counts as grounded (HashUnbound until then).
  5. Round / byte / wall-clock budget; expose GroundedOnArtifacts / Partial / Refused; never render blocked executables.

None of that is glamorous. The glamorous part is the chart in the demo. The durable part is refusing the answer when the sandbox never ran — or when it did, and every artifact failed quarantine.


When the model claims "row count is 12,481" from a hosted CSV you have not hashed yet, what do you return to the caller — the claim, a partial note, or a hard refuse?

The full ladder — binder, claim verifier, offline scenes, and 148 tests — lives in the complete project.

Get the complete project

ZIP + in-depth PDF live in Tech Skill Builder: full .NET 10 solution (hosted-code-interpreter-dotnet), InputPolicy, OutputQuarantine, ArtifactHashBinder, GuardedCodeInterpreterClient, offline fake client, and scenarios this post deliberately leaves closed.

Tech Skill Builder is only $5/month for a very limited time (normally $10). Grab it now before it goes back to $10.

Join Tech Skill Builder

Top comments (0)