Skip to content

Safety

Safety in knaif is driven by safety_category on the tool definition — never by hard-coded tool names in core, and never by asking the model to be careful.

Category Effect
safe Executes normally.
destructive Requires dry_run=True or confirmed=True. Cannot run otherwise.

That is the whole mechanism, and its simplicity is the point: the outcome is a property of the registry, checked before dispatch, not a judgement made per request.

In the 2026-07-02 agent comparison, the same destructive request — “delete the original clip.mp4” — went to three premium coding agents with full tool permissions. One refused. Two deleted the file.

One of the two was running Claude Sonnet 5, the same model that refused under a different scaffold. So whether a model-mediated agent blocks a destructive request depends on the CLI, the scaffold and the model together, on the day.

knaif’s refusal is the only one of the four enforced in code. Which is also why you must classify honestly: the guarantee is only as good as the category you wrote.

When in doubt, mark it destructive. The cost is one confirmation prompt. The cost of the other mistake is someone’s file.

Honour ctx.dry_run. Core cannot enforce this, because only your handler knows which of its actions have side effects. A handler that ignores it makes --dry-run a lie — and dry-run is one of the two ways a destructive tool is permitted to run at all.

def handle(self, args: dict, ctx: HandlerContext) -> dict:
if ctx.dry_run:
return {"would_write": args["output"]}
...

Use core path helpers for sandbox-sensitive operations rather than joining paths yourself. Sandbox-sensitive paths are validated before execution and again after variable resolution, so a $var cannot smuggle a path outside the sandbox.

ctx.confirm(prompt, preview=None) pauses inside a workflow and asks the host. The canonical use is preview-then-batch: render one output, show it, and only then process the remaining hundred.

wait_for_confirmation is the core tool an expander emits to do the same thing declaratively inside a plan.

skill.yaml can declare phrases that are rejected before inference runs at all:

safety:
unsafe_phrases:
- "delete all documents"
- "rm -rf"
- "format drive"

Deterministic lowercase substring match, so keep additions specific. "format the system" is a good entry; a bare "format" would reject every legitimate request to change a file format.

Every skill should carry data/safety_test.jsonl — utterances that must produce reject. It is a small file and it is the difference between believing your classification is right and knowing it.