Deterministic Prompt Hashing For Ai Trust In Devsecops
Written by
Vera Crypt
The problem I ran into: “Trust” that changes every run
I spent a weekend trying to make AI outputs auditable in a CI/CD pipeline. The goal was simple on paper: treat an AI call like a build artifact—repeatable, explainable, and checkable for risk.
What I discovered was painful: even with the same prompt text, models can behave slightly differently across time (sampling changes, tool execution order changes, context differences, retrieval differences). That makes it hard to answer a boring but essential question:
“Did this exact AI behavior run in this exact pipeline, with this exact context?”
My workaround became a small but powerful practice: deterministic prompt hashing with a “risk ledger” that records exactly what input (and derived inputs) the model saw. This is a practical slice of AI Trust, Risk & Security Management: not “trust the model,” but “trust the inputs and the execution metadata you can prove.”
The approach: hash the prompt and the execution context deterministically
Instead of hashing only the user prompt, I hash:
- System prompt (developer instructions)
- Tool schema (function names and JSON schema)
- Retrieval context (the chunks that get inserted)
- Generation settings (temperature, top_p, max tokens)
- A stable “message normalization” step so semantically identical structures produce identical hashes
Then I compute a SHA-256 digest and store it alongside a risk evaluation result.
Why this matters for AI Trust:
- The hash becomes a “fingerprint” for the request.
- The risk ledger becomes evidence for policy enforcement (e.g., block requests containing disallowed retrieval sources).
- When something changes, you can detect it before the model runs.
What I implemented: a deterministic request hasher + a risk gate
Requirements
This example uses:
jsonfor canonical structure buildinghashlibfor SHA-256- a tiny risk policy that blocks requests containing certain markers in the retrieval context
Step 1: normalize and canonicalize JSON
JavaScript and Python can produce different JSON string orderings depending on insertion order. Hashing the raw JSON string can lead to different digests for equivalent data.
So I built a canonical serializer using:
- recursively sorted object keys
- stable list ordering (assuming upstream ordering is already deterministic)
Code (Python)
import hashlib import json from typing import Any, Dict, List def _canonicalize(obj: Any) -> Any: """ Convert Python structures into a deterministically ordered structure. - Dict keys are sorted. - Lists keep their order (you must ensure upstream order is stable). - Primitive values are returned as-is. """ if isinstance(obj, dict): return {k: _canonicalize(obj[k]) for k in sorted(obj.keys())} if isinstance(obj, list): return [_canonicalize(x) for x in obj] return obj def sha256_canonical_json(payload: Dict[str, Any]) -> str: """ Create a SHA-256 hash of a deterministically serialized JSON payload. """ canonical_obj = _canonicalize(payload) canonical_json = json.dumps(canonical_obj, separators=(",", ":"), ensure_ascii=False) return hashlib.sha256(canonical_json.encode("utf-8")).hexdigest()
Why each block exists
_canonicalize: ensures key ordering can’t accidentally change the hash.separators=(",", ":"): avoids whitespace differences.ensure_ascii=False: keeps unicode stable (but still canonicalizes structure).
Step 2: define the exact “AI request fingerprint”
I represent the AI request as a single dictionary that includes everything I want to be able to audit.
Code (Python)
def build_ai_request_fingerprint( system_prompt: str, user_prompt: str, tool_schemas: List[Dict[str, Any]], retrieval_chunks: List[Dict[str, str]], generation_settings: Dict[str, Any], model_name: str, ) -> Dict[str, Any]: """ Returns a structured payload suitable for deterministic hashing. """ return { "model": model_name, "generation": generation_settings, "prompts": { "system": system_prompt, "user": user_prompt, }, "tools": { # Keep tool order stable: upstream should already provide deterministic order. "schemas": tool_schemas, }, "retrieval": { "chunks": retrieval_chunks, }, }
Step 3: implement a risk policy that checks the fingerprint inputs
For demonstration, I implemented a simple policy:
- Block if any retrieval chunk contains the marker
SENSITIVE_SOURCE:LEAK - Block if system prompt contains
exfiltrate(case-insensitive)
In real systems, this gets wired to:
- threat intelligence
- allow/deny lists of sources
- internal prompt policies
- data classification checks
Code (Python)
def evaluate_risk(retrieval_chunks: List[Dict[str, str]], system_prompt: str) -> Dict[str, Any]: """ Tiny risk engine for demonstration. Returns a structured decision with a reason. """ reasons = [] sys_lower = system_prompt.lower() if "exfiltrate" in sys_lower: reasons.append("system_prompt_contains_exfiltrate") for i, chunk in enumerate(retrieval_chunks): text = chunk.get("text", "") if "SENSITIVE_SOURCE:LEAK" in text: reasons.append(f"retrieval_chunk_marked_sensitive_leak_at_{i}") decision = "allow" if not reasons else "deny" return {"decision": decision, "reasons": reasons}
Step 4: wire it together like a CI step
In my pipeline, this function runs before any model call.
It:
- builds the fingerprint payload
- hashes it deterministically
- evaluates risk based on the same inputs
- returns a ledger record you can store in logs/artifacts
Code (Python)
def build_risk_ledger_record( system_prompt: str, user_prompt: str, tool_schemas: List[Dict[str, Any]], retrieval_chunks: List[Dict[str, str]], generation_settings: Dict[str, Any], model_name: str, ) -> Dict[str, Any]: fingerprint_payload = build_ai_request_fingerprint( system_prompt=system_prompt, user_prompt=user_prompt, tool_schemas=tool_schemas, retrieval_chunks=retrieval_chunks, generation_settings=generation_settings, model_name=model_name, ) request_hash = sha256_canonical_json(fingerprint_payload) risk = evaluate_risk(retrieval_chunks=retrieval_chunks, system_prompt=system_prompt) return { "request_hash_sha256": request_hash, "model": model_name, "risk": risk, "fingerprint_payload_preview": { # Keep logs small; real pipelines store full payload in protected artifacts. "generation": generation_settings, "prompts": {"system": system_prompt[:60], "user": user_prompt[:60]}, "retrieval_chunk_count": len(retrieval_chunks), "tool_schema_count": len(tool_schemas), }, }
End-to-end demo: same data → same hash; risky input → deny
Here’s a runnable script that shows:
- identical request inputs produce identical hashes
- changing only retrieval text changes the hash
- risk policy denies when it sees a marker
Code (Python)
if __name__ == "__main__": model_name = "example-llm-v1" system_prompt = ( "You are a secure assistant. Do not exfiltrate data. " "Only use provided retrieval context." ) user_prompt = "Summarize the incident timeline." tool_schemas = [ { "name": "create_ticket", "description": "Create a ticket in the internal tracker.", "parameters": { "type": "object", "properties": { "title": {"type": "string"}, "severity": {"type": "string"}, }, "required": ["title", "severity"], }, } ] generation_settings = { "temperature": 0.0, "top_p": 1.0, "max_tokens": 300, } retrieval_chunks_a = [ {"id": "chunk-1", "source": "SOC", "text": "INCIDENT: auth failures spike at 10:15 UTC."}, {"id": "chunk-2", "source": "Logs", "text": "MITIGATION: throttling enabled for affected routes."}, ] record_a1 = build_risk_ledger_record( system_prompt=system_prompt, user_prompt=user_prompt, tool_schemas=tool_schemas, retrieval_chunks=retrieval_chunks_a, generation_settings=generation_settings, model_name=model_name, ) record_a2 = build_risk_ledger_record( system_prompt=system_prompt, user_prompt=user_prompt, tool_schemas=tool_schemas, # same order retrieval_chunks=retrieval_chunks_a, # same text generation_settings=generation_settings, model_name=model_name, ) print("A1 hash:", record_a1["request_hash_sha256"]) print("A2 hash:", record_a2["request_hash_sha256"]) print("Hashes match:", record_a1["request_hash_sha256"] == record_a2["request_hash_sha256"]) print("Risk:", record_a1["risk"]) # Change retrieval context slightly retrieval_chunks_b = [ {"id": "chunk-1", "source": "SOC", "text": "INCIDENT: auth failures spike at 10:16 UTC."}, {"id": "chunk-2", "source": "Logs", "text": "MITIGATION: throttling enabled for affected routes."}, ] record_b = build_risk_ledger_record( system_prompt=system_prompt, user_prompt=user_prompt, tool_schemas=tool_schemas, retrieval_chunks=retrieval_chunks_b, generation_settings=generation_settings, model_name=model_name, ) print("\nB hash:", record_b["request_hash_sha256"]) print("A vs B hashes match:", record_a1["request_hash_sha256"] == record_b["request_hash_sha256"]) # Risky input: marker in retrieval text retrieval_chunks_c = [ {"id": "chunk-1", "source": "SOC", "text": "SENSITIVE_SOURCE:LEAK key material fragment detected."} ] record_c = build_risk_ledger_record( system_prompt=system_prompt, user_prompt=user_prompt, tool_schemas=tool_schemas, retrieval_chunks=retrieval_chunks_c, generation_settings=generation_settings, model_name=model_name, ) print("\nC hash:", record_c["request_hash_sha256"]) print("C risk:", record_c["risk"])
What happens when I ran it
- A1 hash == A2 hash because inputs are identical and canonicalized.
- B hash differs because the retrieval text differs.
- C risk is denied because the retrieval chunk includes
SENSITIVE_SOURCE:LEAK.
This gives me a trustworthy workflow in DevSecOps terms:
- I can block AI calls before they happen based on deterministic, inspectable input.
- I can store the request hash as evidence of what was executed.
How this fits Zero Trust and DevSecOps
This isn’t “zero trust for the model.” It’s zero trust for the pipeline’s claims about what it fed the model.
- Preemptive defense: the risk gate runs before the model call.
- Policy binding: hash + risk ledger ties decisions to specific inputs.
- Reproducibility: the request hash helps detect drift in prompts, retrieval, or tool schemas.
In practice, I also enforce:
- only allow tool schemas from a signed artifact
- require retrieval sources to be allowlisted
- store the full fingerprint payload in a protected CI artifact tied to the commit
Closing summary
I built deterministic prompt hashing and a small risk ledger so AI requests become auditable build artifacts instead of “whatever the model decided to do this time.” By hashing a canonical representation of the system prompt, generation settings, tool schemas, and retrieval context—and denying execution when simple risk markers appear—I gained a concrete, pipeline-friendly way to manage AI trust and security without relying on vibes or hoping inputs stayed the same.