Spec-driven detection engineering
Agents made the query cheap. The spec keeps it threat-aligned. Why we re-released opentide as a CLI for agentic detection engineering, and what comes next.
By opentide
opentide is spec-driven development for detection engineering. An ontology and a taxonomy say what a detection is. opentide, the package you pin, checks every change against them, on a laptop and in CI. Your detection repository stays the source of truth.
We re-released opentide as a CLI on 9 September. This post explains why, and where it goes next.
Agents made the query cheap
Intelligence moves faster than detection engineering can. Advisories, CVEs and campaigns arrive every day. The number of people who can turn a report into a validated, deployed detection does not grow with them.
Agents changed part of that. A model writes a KQL or SPL query in seconds. Everything around the query stayed expensive: does it map to a real threat, does it duplicate a rule you already have, is it valid on the platform, is it deployed where it should be, who reviewed it.
That work needs something to stand on:
- A spec. An ontology and a taxonomy for what a detection is.
- Threat alignment. Every rule traces to an objective and a threat.
- Quality gates. Strict validation in CI, for humans and agents alike.
- Maintainability. One model across seven platforms, versioned in git.
Without them, detection engineering breaks in predictable ways at scale. The same rule gets described two ways. Rules exist that nobody can trace to a threat. One idea is rewritten per SIEM and EDR. Engines are shared by copying them into every repository. Scripts, and now agents, edit YAML with no schema and no gate. That last one gets worse with every agent you add.
What spec-driven means here
the spec · ontology + taxonomy
specifications
normative · CC-BY-4.0
- ontology
- threat::1.0 · objective::1.0 · rule::1.1
- taxonomy
- 35 versioned vocabularies · fixtures · RFCs
spec markdown → Pydantic models → JSON Schema, generated and never hand-edited
the package you pin
opentide
pip install opentide · EUPL-1.2
- one package
- CLI, Python SDK, MCP server and every integration
- one model
- Types and JSON Schema come from the same models, so they cannot drift
validates · generates · deploys · documents
the source of truth
your detection repo
yours · in git
- objects/
- threats, objectives and rules as YAML
- .opentide/
- platform configuration, generated schemas and templates
- git
- your CI, your reviews, your history
private or public
The spec does not ship detections. The package does not hold your YAML. Your repo stays the source of truth.
The ontology is three objects that answer three questions:
- A threat (
threat::1.0): what do we defend against? - An objective (
objective::1.0): what are we trying to detect, broken down into signals? - A rule (
rule::1.1): how do we detect it, on which platform?
References point back by UUID: a rule's detection_model names its objective, and an objective lists its threats. Coverage flows the other way. See the object model.
The taxonomy is 36 versioned vocabularies: severity, impact, leverage, viability, surface, kill chain, signal entities, ATT&CK and more. A threat cannot say severity: High when the scale runs from Localised incident to National cyber emergency. opentide validate --strict stops it and names the file and line.
The spec lives in specifications under CC-BY-4.0, and every significant change goes through an RFC. opentide implements it. Its JSON Schema is generated from the same Pydantic models as the code, so the two cannot drift.
That is also why coverage stops being a heatmap. "We cover 85% of ATT&CK" cannot say for which threat, with which signal, or on which platform. A graph of threats, objectives, signals and rules can, and a threat that no objective covers shows up as a gap.
Why a CLI
opentide is not new. As CoreTide, it ran in production for more than two years before it was open-sourced in February 2025. It lived inside each detection repository as a git submodule. That shape worked for one team. It did not work for many, and it could not serve agents.
Our own issue trackers made the case:
- Client CI pulled whatever the engine was that day: "Looks like commit 9ef7a58 introduced a new bug." (CoreTide#3)
- The CI template included a file from CoreTide that included more files, and broke on GitLab. (InitTide#5)
- The template's own sample rule did not pass validation. Expected behaviour, but "it can be (understandably) confusing for newcomers". (InitTide#12)
python Orchestration/validate.pyfailed outside CI. (CoreTide#61)- The data model was maintained three times over, a "triple-maintenance data framework" of YAML metaschemas, Python dataclasses and manual conversion. (CoreTide#65)
- "CoreTide currently has zero unit tests — validation is entirely pipeline-driven." (CoreTide#71)
In April we wrote the plan down as Project TideKit (CoreTide#60): turn the submodule into "a versioned Python package on PyPI with a CLI, MCP server, and clean API surface." The plan was written to be executed by an autonomous agent fleet, and it was. The new repository was created on 22 June, and phases 0 to 9 were merged into it the same day.
A few decisions shaped it:
- A new repository, not a rewrite in place. Every pinned submodule SHA keeps resolving while instances migrate.
- One entry point.
opentide, withsetup,validate,lint,generate,deploy,share,info,rules,migrateandmcp. - Platforms are repository configuration.
opentide setupenables them per repository. They are not picked at install time (#107). - Onboarding is a command.
opentide setupreplaces the template repository and writes CI for GitHub, GitLab or Azure DevOps (#64, #28). - Migration is a guide, a prompt and one layout command. Moving off CoreTide is a documented path:
opentide migrate objectsmoves the old directory layout (#100), and an agent prompt handles the rest, reviewed as a diff (#104).
The result is a tool on your PATH instead of scripts in your tree:
| Ergonomics | Submodule era | opentide CLI | Evidence |
|---|---|---|---|
| Install | Clone, init the submodule, patch sys.path | pip install opentide, Python 3.10 to 3.14 | CoreTide#60 |
| Upgrade | CI pulled whatever the engine was that day | Pin a version, read the changelog | CoreTide#3 |
| Onboard | Copy a template; the sample rule failed | opentide setup, as a wizard or with flags | InitTide#12, #64 |
| Operate | Orchestration scripts that only ran in CI | One command tree; --help in about 0.2 s | CoreTide#61, #142 |
| Automate | CI glue through remote includes | opentide setup ci for GitHub, GitLab and Azure DevOps | InitTide#5, #28 |
| Read results | Logs, plus a legacy exit code 19 | --json, exit codes 0, 1 and 2, file and line | #135, #139 |
| Work with agents | Agents shell out and guess | An MCP server and skills, wired by setup | CoreTide#68, #59 |
On 9 September we published 0.1.0 and archived six repositories. Each one became one part of a single stack:
| Archived | Became |
|---|---|
| CoreTide | opentide: the CLI, SDK and MCP server, in one package |
| InitTide | opentide setup |
| ShareTide | library |
| WikiTide | opentide generate docs and the explorer |
| AgentTide | skills |
| SyntaxTide | the language server, in progress |
History stays, and pinned SHAs keep working. New work goes into the package.
Built for agents
An agent is only as good as the spec it is held to.
01A contract it can read
submodule era · JSON Schemas generated in-tree; no separate spec
opentide · Normative specs, generated JSON Schema, 35 versioned vocabularies
02Feedback it can act on
submodule era · Scripts and logs to interpret
opentide · --json results, exit codes, file:line findings
03Tools it can call
submodule era · None: shell out to scripts
opentide · MCP: search, chaining, coverage, validation, dry-run deploy
04Knowledge it can load
submodule era · Skills and an AGENTS.md in a separate repo, copied in by hand
opentide · The same skills and an agent entry point (AGENTS.md or CLAUDE.md), installed by opentide setup skills
05Gates it cannot skip
submodule era · CI glue rewritten per instance
opentide · setup ci: strict validate and lint, live query checks, staging first
| what an agent needs | submodule era | opentide package |
|---|---|---|
| 01A contract it can read | JSON Schemas generated in-tree; no separate spec | Normative specs, generated JSON Schema, 35 versioned vocabularies |
| 02Feedback it can act on | Scripts and logs to interpret | --json results, exit codes, file:line findings |
| 03Tools it can call | None: shell out to scripts | MCP: search, chaining, coverage, validation, dry-run deploy |
| 04Knowledge it can load | Skills and an AGENTS.md in a separate repo, copied in by hand | The same skills and an agent entry point (AGENTS.md or CLAUDE.md), installed by opentide setup skills |
| 05Gates it cannot skip | CI glue rewritten per instance | setup ci: strict validate and lint, live query checks, staging first |
A submodule gave agents scripts to run. A package gives them a spec to follow.
A CLI is the most universal tool interface an agent has, so the contract matters. With --json, every command writes exactly one JSON document to stdout. Diagnostics go to stderr, and exit codes are 0, 1 or 2. A check that a platform cannot run fails, and says why:
$ opentide --json validate query --platform crowdstrike
{
"platform": "crowdstrike",
"valid": null,
"supported": false,
"ok": false,
"status": "failed",
"message": "query validation not supported for crowdstrike"
}
$ echo $?
1The loop has two zones. In the first, an agent searches the graph, reads the lineage with get_chaining, drafts YAML with the skills, runs validation_report until it is clean, checks queries against the tenant with validate query --live, and previews the deploy with deploy_rule(dry_run=true). Nothing in that zone deploys. Its output is a pull request. In the second zone, people own every gate: the review, and the merge that promotes to production. The agentic setup guide has the details.
We hold our own tools to the same bar. On 22 September, a first-user simulation against 0.3.0 filed twenty issues (#239 to #258). Two of them were tool results an agent would take for success. MCP validate_query returned valid: true for garbage queries (#245). And run_query returned rows: 0 (#258), "which an agent reads as 'the query ran and matched nothing'" (#275). All twenty were closed that day and shipped in 0.4.0. Since 0.11.0, a query check is a check against the tenant: a bare validate query refuses and names --live (#456).
How we build it
We build opentide the way we want detections built. Between 0.1.0 on 9 September and 0.12.1 on 9 October we shipped 28 releases. As of 12 October, 160 pull requests have been merged since 9 September, the day 0.1.0 shipped, and 142 of them were drafted on cloud-agent branches. The same CI checks every pull request: tests on Python 3.10 to 3.14, an 85% coverage gate, and CodeQL.
What comes next
The north star: intel to a reviewed, deployed detection in under an hour, with a human approving every promotion.
target sub-hour from intel to STAGING, with a human approving every promotion
- agents + opentide
- waiting on a human
Intel lands
an advisory, a report,
a ticketAgent drafts
the chain, in YAML
skills + MCPopentide checks
validate --strict
validate query --livePR opened
branch + diff
CI gates runSTAGING
from the pull request
--plan STAGINGHuman review
a reviewer
approves the mergePRODUCTION
on merge
--plan PRODUCTION
Four directions, each tracked in a public issue or RFC:
- in progress
- rfc accepted
- planned
- proposed
- 012 items
Integrations
More places the same rule runs.
- 023 items
In the editor
The spec where you type.
- 033 items
A stronger model
Richer rules, cleaner state.
- 043 items
Toward 1.0
A stable surface, fewer legacy paths.
Start
pip install 'opentide[mcp]'
opentide setup --yes --platform sentinel --ci github
opentide setup mcp --claude-code --yes
opentide setup skills --claude-code --yes --install opentide-detection-rule
opentide validate --strictThat is a detection repository with Sentinel and GitHub CI, an agent wired to the MCP server, the rule-authoring skill installed, and the same strict gate your CI runs. Generated pipelines install the exact version you ran setup with, so upgrades arrive as a diff you review.