Fit the Ops Context Without Flooding the Prompt
I spent two weeks getting inconsistent JSON output from Claude for log parsing until I realized my prompt described the format but never showed it an example. We were pulling firewall events from FortiOS 7.4.3, normalizing them on Ubuntu 22.04, and feeding the results into a Python 3.11 enrichment script that tagged authentication failures, VPN anomalies, and policy violations before our SIEM touched them.
My first prompt tried to explain our whole environment: plant floor VLANs, maintenance laptops, jump hosts, privileged accounts, shift schedules, and vendor VPN exceptions. I thought more background would make the model behave like one of us. I was wrong. The prompt became a soft pile of context, and the output drifted whenever a log line had a missing field or an odd FortiGate action string.
Less context worked better.
The useful pattern was to give Claude only the operational boundaries that affected the decision. I stopped describing every subnet and started naming the exact fields it had to read, the classifications it could use, and the values it had to leave untouched. For our environment, that meant source IP, destination IP, user, action, policy ID, VPN tunnel, timestamp, and raw message. Everything else went into retrieval or a lookup table outside the prompt.
My opinion after six months is simple: ops prompts should be narrow enough to audit at 2 a.m. during an incident, because clever context that nobody can reason about becomes another production dependency.
Show the Shape Before Asking for Structured Output
The breakthrough came when I added one concrete example before asking for generated JSON. I did not add a longer explanation. I did not add a taxonomy. I showed Claude one ugly log line and one perfect output object, including null handling, lower-case enum values, and the exact nesting our parser expected.
That single change moved structured output reliability from 71% to 96%. We measured it across 500 FortiOS 7.4.3 samples with missing usernames, duplicate keys, noisy VPN messages, and three known malformed timestamp patterns. Reliability meant valid JSON, accepted enum values, no invented fields, and a schema match in our Python 3.11 validator.
The example beat the lecture.
prompt = """
You classify FortiGate log events for our security operations queue.
Return only JSON matching this shape.
Example input:
date=2026-04-18 time=02:14:55 devname=fg-prod action=deny srcip=10.44.12.91 dstip=172.20.8.10 policyid=0 user="" msg="Denied by forward policy check"
Example output:
{
"event_type": "policy_denial",
"severity": "medium",
"user": null,
"source_ip": "10.44.12.91",
"destination_ip": "172.20.8.10",
"policy_id": 0,
"needs_human_review": false
}
Now classify this input:
{log_line}
"""
What I didn’t expect was how much the example reduced arguments inside my own team. Once we could point to a visible output contract, we stopped debating whether “clearer wording” would help and started testing whether the example covered the failure modes we actually saw.
For structured ops work, I now treat few-shot examples as the real specification and the prose as supporting material. That is the only prompt engineering habit I would defend in a change review without hedging.
Separate System Rules From Task Instructions
I use system prompts for stable behavior and user prompts for the specific job. In our log pipeline, the system prompt says we are doing security operations support, output must be machine-readable, unknown values must stay unknown, and the model must never create IP addresses, usernames, hostnames, or ticket IDs. The user prompt carries the current log line, allowed labels, and any incident-specific context.
This split matters because ops automation changes under pressure. During a VPN outage, I may need to add a temporary label for known vendor reconnect storms. During a phishing run, I may need different severity handling. I do not want those incident-specific tweaks mixed into the long-lived behavioral contract.
Boundaries saved us.
- I put schema discipline in the system prompt.
- I put examples near the task input where they are easiest to update.
- I put environment facts in retrieval, not in static prompt prose.
- I put allowed labels in a short explicit list.
- I put escalation language outside the model when deterministic routing is safer.
I also learned where not to use elaborate prompting. I over-engineered a chain-of-thought prompt for a simple classification task — the extra reasoning tokens cost 4x more and made the output less consistent, not more. We needed a label, a confidence score, and two fields copied from the log. Asking for hidden reasoning made a small job behave like a committee meeting.
My rule now is blunt: if the operation is classification, extraction, or reformatting, I want short instructions, visible examples, and strict validation, not a philosophical prompt.
You may also find this useful: Check out our guide on Python Network Config Backup: Automating Multi-Vendor Device Snapshots for more practical tips.
Ground the Model in Evidence We Can Verify
Manufacturing environments punish hallucination because asset names, vendor accounts, and firewall policies often look strange even when they are legitimate. We have PLC engineering stations that only wake up during maintenance windows, vendor VPN users that connect once a quarter, and NAT rules old enough to have survived three firewall refreshes. A model that fills gaps with plausible guesses is dangerous in that environment.
I ground prompts by forcing the model to quote or copy the evidence it used. For alerts, I ask for the specific log fields that support the classification. For change reviews, I provide the exact diff, the affected policy IDs, and the known business owner. For runbook suggestions, I include the approved command set and tell Claude to mark anything outside that set as “operator decision required.”
No evidence, no action.
The best grounding pattern has been a three-part input: raw evidence, allowed reference data, and the requested output. I keep those visually separate in the prompt because mixing them invites the model to treat our instructions like facts or our facts like optional guidance. When the reference data says policy 1842 belongs to packaging line remote access, the model can use that. When the raw log lacks a username, the model must return null.
I do not trust model confidence by itself. I trust copied fields, constrained labels, schema validation, and boring post-processing. In ops, boring is not a weakness; boring is how I sleep after deploying automation near production systems.
Treat Prompts Like Production Code
Prompt versioning became non-negotiable once more than one analyst depended on the output. We store prompts beside the parser code, review them in pull requests, and tag them with the same release notes as the Python 3.11 service that calls Claude. Each prompt has a version, a fixture folder, and a small regression set built from real FortiOS 7.4.3 logs with sensitive values replaced.
My team tracks prompt changes the same way we track detection logic changes. If a prompt update changes severity distribution, enum usage, null frequency, or JSON failure rate, we look at it before shipping. The diff may be plain text, but the blast radius is still production behavior.
Text changes are code changes.
The most useful review habit is asking what the prompt forbids. Good ops prompts do not only say what to produce; they say what not to invent, when to return null, when to ask for human review, and which fields must be copied exactly. I have seen one vague sentence create a week of cleanup because the model started “normalizing” hostnames that our CMDB needed in their original form.
I prefer prompt change control over prompt heroics. A clean one-shot example, a locked schema, a regression set, and a clear owner beat any clever wording I have tried.

