SuperAPI AI Directory

dym-cti-expert-safety-gates

Security & auditing

View repository

Updated 10-18 01:21

About this skill

Skill: dym-cti-expert-safety-gates — Safety rails: what gets BLOCKED, what gets ASKED, what stays silent

Why use it: so you are not surprised mid-case, and more importantly — so you do not disable the wrong rail. What it produces: produces nothing; it changes how you read every confirmation prompt that cti-expert raises.


Three hooks, and they only exist when installed via the plugin path

hooks/hooks.json registers exactly three entries:

Hook Event captured Behavior
leakguard.py PreToolUse · Write|Edit|NotebookEdit deny — blocks writing case data to a tracked
actionguard.py PreToolUse · Bash|mcp__.* ask — asks before an irreversible outbound action
sessionguard.py SessionStart reports backend tier + warns when the number of @tool changes

bash scripts/register.sh hooks cannot be installed. Only /plugin install cti-expert has them (01 step 3b). Verify with /hooks in Claude Code — you must see 3 entries.

leakguard.py — RULE 1 at WRITE time

The repo forbids investigation data from leaking into tracked files: real people's names, the case's infrastructure, real GA4/GTM, case IDs, operator PII — including in the form of "illustrative examples" in comments or test fixtures.

There are two enforcement layers, intentionally:

  1. scripts/leakcheck.sh runs at git pre-commit (installed via bash scripts/install-hooks.sh, must be run on every fresh clone because .git/hooks/ is not tracked).
  2. hooks/leakguard.py runs at Write/Edit time.

The reason for the second one: the git hook only fires at commit and git commit --no-verify can be bypassed, while an agent writes continuously and rarely commits. At the Write layer there is no --no-verify flag at all.

leakguard.py does not re-install the regex patterns itself — it calls scripts/leakcheck.sh. A second copy would drift, and a guard that has drifted always reports clean.

It only denies when both are true: the path is inside a cti-expert checkout, and git does not ignore that path. Writes to intel_engine/cases/, intel_engine/knowledge/, .env — i.e. the already-gitignored stores — are completely untouched. So are writes outside the checkout.

When you need an example in a tracked file, use an obviously fake placeholder: example.com, registrant@example.com, G-XXXXXXXXXX, CASE-0001, 1ExampleBitcoinAddressDoNotUse.

actionguard.py — outbound gate (ask, not deny)

The list is data, not code: hooks/references/outbound_actions.json. Read directly:

python3 -c "
import json;d=json.load(open('hooks/references/outbound_actions.json'))
print('MCP :', list(d['mcp_tools']['entries']))
for e in d['bash_patterns']['entries']: print('bash:', e['match'], '| flag:', e.get('flag_required'))"

7 MCP tools gated: engage_account, harvest_authenticated, anyrun_submit, capture_evidence, pivot_extract, misp_push, misp_publish.

11 bash patterns gated:

Pattern Why
en_engage.py, intel.py engage sign up/log in on the target's platform — outbound, attributable to you, irreversible
en_harvest.py, intel.py engage-harvest already logged in inside the target's member area
bp_anyrun.py submit, --submit, --confirm-submission submit samples to a public sandbox
sh_misp.py push / publish, intel.py misp push / publish push/publish events outward

Two tools have flag_required — capture_evidence and pivot_extract only ask when there is --submit. Their default path (passive collection) does not get asked. That is intentional: asking too much is the fastest way for a guard to be turned off entirely.

Same logic in detail intel.py engage — the trailing space carries meaning: it keeps intel.py engage-report (local write-up render only) from triggering the prompt.

Not gated, intentionally: detect_login, url_paths, passive_ssl, and all routine collection.

Why the gate is doubled

The gate in the code (submit(confirm=…), Engage's preflight) lives in intel_engine/ — the directory vendored. A three-way sync could revert it without not breaking any test (nearly happened on 2026-08-23). The hook lives in cti-expert's own tree and keys on tool name, so a sync cannot reach it.

Consequence for consumers: if you see a confirmation prompt while the tool has also asked itself — that is not a bug, it is two independent guards.

Both PreToolUse hooks FAIL-OPEN

A hook that dies from an internal error lets it through, it does not block. A broken hook must not be allowed to paralyze every write operation in the repo. The backstop is scripts/audit.sh and the git pre-commit hook.

Verification: tests/test_hooks.py asserts both directions — fires on the right target, silent in the adjacent safe case. Run for real:

python3 tests/test_hooks.py
# PASS — Claude Code hooks (58 checks: RULE 1 write-time gate fires on tracked files and stays
# out of the case stores, outbound actions ask, passive work is silent, both hooks fail open,
# and the plugin wiring is machine-independent)

audit.sh §7 additionally checks that every path hooks.json declared still exists — renaming a script will disable that hook silently.

Never submit the case's own sample to a public sandbox

Submitting a sample to a public ANY.RUN/VirusTotal tells the operator they are under investigation, and it cannot be taken back. The repo has its own test for this:

python3 tests/test_no_sample_submission.py
# PASS — no UNGATED sample-submission path (30 checks: every endpoint either read-only or behind
# the marked-and-enforced confirmation gate, contract stated to the analyst)

Before submitting any hash, run hash-id (see 04): a 32-hex string could be the MD5 of a file or the NTLM of a password. Submitting the latter by mistake leaks credentials.

Principles when a guard annoys you

From CLAUDE.md RULE 6, and it holds for both users and repo editors:

Do not loosen a guard to silence a prompt. Prefer tightening flag_required, or matching more precisely, over deleting a line. A guard you have to turn off to get work done is a guard that will be turned off entirely.

--yolo of the skill drops the skill's interactive prompt of the skill; it does not touch the harness-level hook.