Decision-horizon

Latest version: v0.14.1

Safety actively analyzes 1035937 Python packages for vulnerabilities to keep your Python projects secure.

Scan your dependencies

Page 1 of 4

0.14.1

- **Coding-agent hooks: `~` and `$HOME` in shell commands.** The shell expands them before the command runs, so the
hooks now do too: `rm -rf ~/.dh/records` and `cat $HOME/.dh/signer.key` name the record store and key and are
denied, as their absolute paths already were (Python and Node). A shell command that removes, moves or truncates
(`rm`, `rmdir`, `mv`, `unlink`, `shred`, `truncate`, `find -delete`) now protects the store's ancestors too, as a
`delete_*` tool did, so `rm -rf ~` is denied.
- `dh replay` (text and HTML) states Ostiary's signed score: model, mode, decision, risk, top reasons, and whether it
blocked the call or sent it to the checkpoint.
- Export: a call Ostiary blocked says so first (`blocked by Ostiary at risk 0.78; the policy said allow (rule …)`) in
OCSF `status_detail` and the OTEL span status.
- PDF report wording: a single chain reads "The chain verified"; "1 decision was".

0.14.0

- **PDF audit report** (`dh export --format pdf --out audit.pdf`, `pip install "decision-horizon[pdf]"`): verified
chains as a branded report for auditors and control owners. Letterhead with your organisation
(`--report-org`), title and recipient (`--report-title`, `--report-for`), A4 or US Letter; the verification
statement; totals; a needs-attention section explaining every blocked call, checkpoint, Ostiary flag and
cross-session credential from its record; every session's decisions; every record hash; how to check it. The
report carries an events digest equal to sha256 of the jsonl export of the same chains, so anyone can prove which
records it was rendered from. Fonts (IBM Plex Sans, JetBrains Mono, SIL OFL) are bundled.

0.13.0

- **CoSAI AITF attributes on OTEL spans**: every span also carries the CoSAI WS2 AI Telemetry Framework (proposal
v0.4) attributes the signed records can fill: `agentic_log.*` (outcome, Ostiary score as `anomaly_score`, the
policy check), `identity.approval.*` (decision, approver, approver type, `prompt_hash` of what the approver saw,
trigger), `security.mediation.*` and `security.attribute_source.*`.
- `dh-export/1` gains `hitl_trigger` (policy or ostiary), `hitl_shown_hash` and `hitl_decided_at`.

0.12.0

- **OpenTelemetry export** (`dh export --format otel`, `--otlp-endpoint URL`): one span per decision, named and
attributed per the OpenTelemetry GenAI semantic conventions (`execute_tool <tool>`, `gen_ai.tool.name`,
`gen_ai.tool.call.id`, `gen_ai.agent.*`, `gen_ai.conversation.id`), one trace per session, span id taken from the
signed intent record's hash, status ERROR with `error.type` for a blocked or failed call, and every `dh-export/1`
field as `dh.*`. Sent over OTLP/HTTP (JSON) to `/v1/traces` with headers from `DH_OTLP_HEADERS` or
`OTEL_EXPORTER_OTLP_HEADERS`; https required, redirects refused, header values scrubbed, rejected spans count as a
failure. Works with `--follow --cursor`. Checked against the OpenTelemetry Collector's OTLP receiver.
- **Ostiary in every export format**: `dh-export/1` gains `risk_score`, `risk_decision`, `risk_reasons`, `risk_mode`
and `risk_model` (from the signed `policy.risk`; null when the gate was off), so OCSF (`unmapped.dh`), CEF
(`dhRisk*`), syslog, Splunk and OTEL all carry the score and reasons.

0.11.0

- **Coding agents** (`dh hooks install <agent>`, `dh hook <agent>`): Decision Horizon on every tool call of Claude Code,
Cursor, GitHub Copilot (CLI, cloud agent, VS Code), OpenAI Codex CLI, Gemini CLI, Windsurf, Cline, Kiro, Factory,
Continue CLI, Augment, OpenCode and Amp, through each agent's own hook system: horizon, policy, Ostiary and a signed
intent before the tool runs (answered as allow / deny / ask in the agent's format), a signed outcome after. One chain
per agent session; parallel hook processes serialise on a lock; a chain changed behind the session is refused, not
forked. Deleting the records, reading the key or editing the agent's own hook configuration is denied. A call the
user declines is `unreported` at close; `dh hooks close-idle` for agents without a session-end event. `[hooks]
on_error` (default deny). Insertion point `agent_hook`, with its own coverage statement.
- **Claude Agent SDK** (`decision_horizon.claude_agent.DecisionHorizon().hooks()`): the same engine as in-process
callbacks; records identical to the Node SDK's `DecisionHorizonHooks`.
- `dh hooks install` creates `~/.dh/hooks.toml` and a signing key on first use. Shared factories moved to
`decision_horizon.factory` (no MCP import), so a hook call takes about 0.15 s.

sdk-ts 0.7.1 — 2026-10-05

- `DecisionHorizonHooks`: `~`, `$HOME` and `${HOME}` in a shell command expand to the home directory, and a shell
command that removes, moves or truncates protects the record store's ancestors, as in the Python hooks.

sdk-ts 0.7.0 — 2026-10-04

- **Claude Agent SDK, in process** (`DecisionHorizonHooks`): `options.hooks` for `anthropic-ai/claude-agent-sdk`'s
`query()` (`PreToolUse`, `PostToolUse`, `PostToolUseFailure`). Each Bash, Write, Edit, MCP and other consequential
tool call gets horizon, policy, Ostiary and a signed intent before it runs and a signed outcome after; reads are
allowed and fold into the horizon unless they target a protected path (store, key, Ostiary baselines, `configFile`).
A deny reaches the model with the rule or Ostiary's reasons and the record id; there is no prompt, so a paused call
goes to the `approver` or is denied. `close()` signs each session's terminal record with `unreported` calls. One
chain per agent session, `claude-agent-sdk-<id>`, insertion point `agent_hook:claude-agent-sdk`. A failure to record
denies the call (`onError: "allow"` lets it through). The session lives in memory: no state files. The records equal
the Python `decision_horizon.claude_agent` ones field for field (parity test), and both verifiers accept both chains.
The Agent SDK is a devDependency used to type-check the callbacks; it is never imported at runtime.
- `Interceptor.intent(tool, args, who)` / `outcome(state, result, status, error?, { extra, clearWindow, observeResult })`:
the two halves of `call()` under separate lock acquisitions, for insertion points where the agent runs the tool in
between; `call()` composes them and its records are unchanged. Also `insertionPoint`, `extraAbstain`, `extraAction`,
`knownTools: "*"`, `observe(..., { resolvable: false })`, `closeSession(extra)`, public `isProtected` / `isSensitive`.
- Coverage: insertion point `agent_hook`, with the Python wording, and `tool_surface.basis`.
- Helpers shared with the Python hooks: `toolKind`, `TOOL_KINDS`, `CANON`, `rel`, `normaliseArgs`, `shellPaths`,
`shlexSplit`, `agentTextOf`, `sessionName`, `denyReason`.
- `pyRealpath` moved to its own module (still exported), so the browser build of Ostiary no longer bundles the
Interceptor's source; the Console's inlined copy is rebuilt with `npm run build:console` (same scorer).

0.10.0

- **Ostiary in the path** (`[ostiary]`, on by default in `advise`): proxy, gateway and Python SDK score each intent
before it runs and sign the score into the record as `policy.risk`. `escalate` sends risky allowed calls to the
approver (who sees the score and reasons); `enforce` denies `block` with `policy.blocked_by = "ostiary"`. The policy
verdict is never rewritten. Baselines persist (0600), are protected from the agent, and a damaged file starts cold.
`mode = "off"` or `DH_OSTIARY=0` turns it off.
- **One switch, every product:** `DH_OSTIARY=advise|escalate|enforce|0` overrides the config for one run of any
command; the SDKs take the mode alone (`ostiary="escalate"`); `dh ostiary` (status, `mode <m>`, `reset`); the proxy
and gateway print one startup line saying what Ostiary will do and how to change it.
- **Console:** an Ostiary page lists every flagged action across sessions; a session shows each record's score, and
"Blocked by Ostiary" where enforce stopped a call. Records made before Ostiary was on are scored in the browser with
an inlined build of the same scorer (`npm run build:console`; a test keeps it identical to the source).
- **Ostiary** (`dh watch`): offline, per-agent behavioural risk scoring of intent records, about 80 µs per action on
one core, no language model. Probability, recommended decision (allow / review / block), top reasons, `cold`
when history is thin. Never allows anything itself: `--webhook`, `--hold` (legal hold in the hosted store) or
your policy act on it. Sensitive content flowing out to a non-internal destination is always a block, whatever the
baseline (defeats baseline poisoning); novelty alone is at most a review and is learned after one; risky actions
are never folded into a baseline. `dh watch --bench` regenerates the labelled stress test: >= 85% precision,
recall, F1 and decision accuracy on every split, including a held-out attack family, renamed personas, unseen
legitimate look-alikes, evasive attacks and baseline poisoning (synthetic corpus; see the README for limits).
- **Automatic upload** (`dh push`, `[cloud]`): proxy, gateway and Python SDK upload each session to the hosted store
as it is written, on by default when `DH_API_KEY` is set, off with `[cloud] enabled = false` or `DH_PUSH=0`; one
startup line says it is on, why and how to turn it off. `dh push --pin | --status | --retry`. At-least-once,
idempotent, never blocks the agent; refused records are marked, a locally truncated chain is reported.

sdk-ts 0.6.0 — 2026-10-04

- Parity with Python 0.10.0: Ostiary (`Ostiary`, `Gate`, `policy.risk` in the TS Interceptor and `Session`, on by
default in advise; identical scores, reasons and baselines files to the Python implementation, proven on 54,934
records), automatic upload (`cloud`, on when `DH_API_KEY` is set, `.push.json` cursors interchangeable with
`dh push`), and `dhjs push` / `dhjs watch`.

sdk-ts 0.5.0 — 2026-10-01

- `decision-horizon/sdk` gets a command line, `dist/cli.js` (minified, imports the bundle; no new dependencies),
installed as `dhjs` and `decision-horizon-sdk`, so `npx decision-horizon/sdk <command>` runs it.
- `keygen --out <path> --kid <kid> [--force]`: an Ed25519 key in the plaintext PEM form `dh keygen` writes
(`<path>` mode 0600, `<path>.pub`); refuses to overwrite without `--force`.
- `verify <chain.jsonl> --pub <file> [--kid] [--head] [--no-meta] [--open] [--json]`: `verifyJsonl` on the file's
bytes, kid and expected head from the sibling `.meta.json` as `dh verify` takes them, `meta_contradicts_chain`
included; the text and `--json` reports equal `dh verify`'s for a chain without witness options. Exit 0 / 1, 2 on
a usage error. `version`, `help`.
- Tests: `sdk-ts/test/cli.test.mjs` (keys interchangeable with the Python side, report parity with `dh verify` on
clean, tampered and truncated chains, the packed tarball run through `npx`); `dist.test.mjs` covers the second file.

Page 1 of 4

© 2026 Safety CLI Cybersecurity Inc. All Rights Reserved.