# Security

Honest framing up front: "100% secure, zero risk" is not a real claim any software
can make. The achievable target, and the one this repo holds itself to, is that
**the only executable surface is one small, dep-free hook you can audit in
under a minute, and every real dependency is declared and monitored, not that
nothing runs.** As of v0.2, something does run: a lifecycle hook fires on every prompt on
Claude Code, and real npm dependencies are declared. The v0.1.0 zero-execution and
zero-dependency posture no longer applies; this document states the real model
plainly, including what changed and why the new model is still trustworthy.

## Posture

1. **One small, dep-free, per-prompt hook, not zero executable surface.**
   `.claude-plugin/plugin.json` registers `hooks/hooks.json`, which wires a
   `UserPromptSubmit` hook to run `node hooks/inject-ruleset.mjs` on Claude Code.
   That script runs on every prompt once installed, this is install-time and
   per-prompt execution, a real change from v0.1.0's claim of nothing running on
   install. `package.json` still has no `postinstall`, no `preinstall`, and no
   npm lifecycle scripts, `hooks/inject-ruleset.mjs` is a host-invoked plugin
   hook, not an npm lifecycle hook. The script itself is intentionally small
   (about 30 lines): it reads a local mode file
   (`~/.config/d-strange/mode`, or `DSTRANGE_STATE_DIR` if set), reads
   `hooks/ruleset.md`, and writes a mode-scaled slice of that file to stdout.
   Node built-ins only, no third-party import, no network call, no child
   process, no filesystem write, no read of any secret or environment variable
   beyond that one whitelisted state-dir key. `node scripts/validate.mjs`
   (`npm test`) and `node scripts/sync.mjs` (`npm run sync`) remain the only
   other first-party scripts, run by a maintainer or CI by hand, unchanged from
   v0.1.0.
2. **Real, declared runtime dependencies, not zero dependencies.**
   `package.json` now declares three runtime dependencies, `commander`,
   `js-yaml`, and `zod`, plus `vitest` as a dev dependency (verified by reading
   it directly, see "how to audit" below). This is a real change from v0.1.0's
   empty dependency graph. `npm audit` currently reports 0 vulnerabilities
   across that graph, and Dependabot is enabled on the repository to monitor it
   going forward and open a PR if that changes. Zero dependencies is no longer
   true; a small, audited, monitored dependency graph is the current, honest
   claim.
3. **Zero telemetry, no phone-home, outside the hook above.** Neither
   `scripts/validate.mjs` nor `scripts/sync.mjs` opens a socket, makes an HTTP
   request, or reads an environment variable, and `hooks/inject-ruleset.mjs`
   does not either, per point 1. The only network activity anywhere in this
   repo happens inside the `hunt`, `council`, and `extract` fan-out workflow
   templates, and only when a user explicitly invokes that tier, using the
   user's own agent tooling (WebSearch or WebFetch), not code shipped by this
   package.
4. **Least-privilege fan-out.** `templates/hunt.workflow.js`,
   `templates/council.workflow.js`, and `templates/extract.workflow.js` each open
   with a `CONTRACT` string handed to every sub-agent they spawn. All three
   contracts say the same three things in the same words: read-only, do not run
   destructive shell, do not read secrets or env, do not POST any data. Writes
   are scoped to a single user-named output path (`outDir` or `outFile` from the
   run's own `args`), never anywhere else in the filesystem.
5. **Auditable in two minutes, hook safety mechanically gated, with a named
   residual limit.** The entire shipped surface is `skills/*/SKILL.md`,
   `references/*.md`, `templates/*.workflow.js`, `commands/*.md`,
   `scripts/*.mjs`, `hooks/*.mjs`, `hooks/*.json`, `hooks/*.md`, and
   `AGENTS.md`, all plain UTF-8 text, no minification, no bundling, no
   compiled artifacts. Three different, narrower checks back this, and it is
   worth being precise about which covers what:
   - `scripts/validate.mjs`'s `OBFUSCATION` regex (`atob(`,
     `Buffer.from(...base64`, `eval(`, `require('child_process'`) runs
     mechanically on every `npm test`, but only against each
     `skills/*/SKILL.md`, not the whole tree.
   - The same script's tree-wide walk covers every shipped
     `.md`/`.mdc`/`.js`/`.mjs`/`.json`/`.txt` file (plus `.clinerules`) but only
     checks those for banned dashes, not for obfuscation patterns.
   - A dedicated `checkHookSafety` gate, also run on every `npm test`, walks
     every `hooks/*.mjs` file and fails the build if it imports anything other
     than a Node built-in, references `fetch`/`http`/`https`/`net`/
     `child_process`/`exec`/`execSync`/`spawn`, uses a dynamic `import()`, or
     reads `process.env` under any key other than `DSTRANGE_STATE_DIR`. This is
     the mechanical enforcement behind point 1's claim about
     `hooks/inject-ruleset.mjs`. **Named limit, stated honestly: this is a
     lexical, regex-based gate, not full static analysis. It cannot catch
     string-concatenation obfuscation of a banned token (for example
     `'impo' + 'rt()'` instead of a literal `import(`), and it does not parse
     an AST.** The trust model for `hooks/*.mjs` therefore rests on two things
     together, not the gate alone: the gate catching the direct, literal form
     of the risky patterns above, and the file staying small enough (currently
     one file, about 30 lines) that every commit touching it gets a real human
     read in PR review, not just a green check.
   - Outside `hooks/*.mjs` and `SKILL.md`, the claim that
     `templates/*.workflow.js`, `scripts/*.mjs`, `commands/*.md`, and
     `AGENTS.md` are free of `eval`, base64 blobs, and dynamic `require` is a
     manual-read inspection claim, true as of this writing and re-checkable in
     two minutes per the audit steps below, not something `npm test`
     mechanically enforces there.

## The 8-point mapping

| Category | How this repo satisfies it |
|---|---|
| Prompt injection | The three fan-out templates are the only place untrusted external content (a fetched web page) ever enters an agent's context. That content is handed to a sub-agent whose `CONTRACT` string instructs it, in the prompt itself, to stay read-only and scoped to fetch plus a single named write path: no destructive shell, no secret/env access, no POST. This is a prompt-level contract, not a capability this repo enforces. `agent()`, `pipeline()`, and `parallel()` are host-provided; whether a sub-agent's actual tool access excludes destructive shell, secrets, or POST depends on the host runtime's own sandbox and permissions, not on anything this repo ships (see Residuals below). If a fetched page's text tries to act as an instruction, the mitigation is that the contract tells the sub-agent not to comply and to keep writes narrowly scoped, not that this repo has removed the underlying capability. |
| Data exfiltration | No code in this repo opens a network connection on its own, including `hooks/inject-ruleset.mjs`, which is pure filesystem, `node:fs`/`node:path`/`node:url`/`node:os` only, verified by reading it in full and mechanically re-checked by `checkHookSafety` on every `npm test`. `scripts/validate.mjs` and `scripts/sync.mjs` are likewise pure filesystem, plus `node:child_process` in `validate.mjs` (for the `--check` syntax probe) only, verified by reading both files in full. The only outbound calls anywhere in the repo are the user-invoked WebSearch/WebFetch inside `hunt`/`council`/`extract`, and every one of those templates' contracts explicitly forbids POSTing data anywhere. |
| Secret detection | `scripts/validate.mjs`'s `OBFUSCATION` regex (`atob(`, `Buffer.from(...base64`, `eval(`, `require('child_process'`) scans every `SKILL.md` on every `npm test` run and fails the build if matched. A separate `checkHookSafety` gate scans every `hooks/*.mjs` file and fails the build on any `process.env` read outside the one whitelisted `DSTRANGE_STATE_DIR` key, so the per-prompt hook cannot read a secret without the gate catching it (regex-based, see the named limit in Posture point 5). Every fan-out template's contract separately and explicitly forbids reading secrets or environment variables. Real runtime dependencies now exist (`commander`, `js-yaml`, `zod`), so the "no third-party code path can reach `process.env`" claim from v0.1.0 no longer holds; `npm audit` is 0 vulnerabilities and Dependabot monitors that graph as the compensating control instead. |
| Dangerous commands | A `UserPromptSubmit` hook now runs on every prompt on Claude Code, so "nothing runs uninvited" is no longer literally true, what is true is that the one thing that runs is mechanically prevented from being dangerous: `checkHookSafety` fails the build if any `hooks/*.mjs` file references `fetch`/`http`/`https`/`net`/`child_process`/`exec`/`execSync`/`spawn`, uses a dynamic `import()`, or imports a third-party module. The only first-party scripts (`validate.mjs`, `sync.mjs`) never shell out to anything destructive; the one `execFileSync` call in `validate.mjs` runs `node --check` against a temp file it wrote itself, and the one in the sync-drift step re-runs `sync.mjs --check` in the same process family, both read-only probes. All three fan-out contracts explicitly forbid destructive shell. |
| Obfuscation | Every shipped file is plain, unminified, human-readable text. `scripts/validate.mjs`'s `OBFUSCATION` regex mechanically enforces this for every `SKILL.md` on every test run (see "Secret detection" above), and `checkHookSafety` separately blocks the dynamic-`import()` and banned-surface patterns inside `hooks/*.mjs`. There is no build step and nothing here is ever minified or bundled, so there is no artifact where obfuscation could hide even if the covered files stayed clean; neither gate is full static analysis, both are regex-based (named honestly in Posture point 5). |
| External fetches | The per-prompt hook itself performs zero fetches, `checkHookSafety` fails the build if `hooks/*.mjs` ever references `fetch`/`http`/`https`/`net`. All real external fetching is confined to the `hunt`, `council`, and `extract` fan-out tiers, and confined further within them to `WebSearch`/`WebFetch`/`Read` per each contract. Those fetches are always user-invoked (the user runs the fan-out tier and, for `hunt`, is asked for a go-ahead before the budget is spent), never automatic, never on install, never on a bare `npm test`. |
| Credential access | No code path in this repo reads an API key, token, or credential: the per-prompt hook is mechanically restricted to the single `DSTRANGE_STATE_DIR` env key by `checkHookSafety`, and `validate.mjs`/`sync.mjs` do not read `process.env` at all. Real runtime dependencies now exist (`commander`, `js-yaml`, `zod`); the mitigation is no longer "zero dependencies" but a small, audited dependency graph (`npm audit`, 0 vulnerabilities) with Dependabot enabled to flag future issues. Every fan-out contract explicitly forbids reading secrets or env in the sub-agents it spawns. |
| Privilege escalation | Nothing here runs with elevated privileges or outside the current process: the per-prompt hook runs as a plain `node` child process the host agent spawns, no daemon, no service, no setuid anything, and it exits immediately after printing to stdout. `sync.mjs` only ever writes inside this repo's own directory tree (`mkdirSync`/`writeFileSync` scoped under `ROOT`), `uninstall.mjs` only ever removes the local mode-state directory (`~/.config/d-strange/` or `DSTRANGE_STATE_DIR`), and `validate.mjs` only reads. |

## Residuals, named honestly

These are not fixed, they are accepted and bounded:

- **`hunt`/`council`/`extract` fetch live web pages.** That content is untrusted
  by construction, a fetched page can say anything, including text engineered to
  look like an instruction. The mitigation here is scope, not detection: the
  fetched content is treated as data for the sub-agent to read and cite, and the
  sub-agent's `CONTRACT` instructs it, at the prompt level, not to turn an
  injected instruction into a destructive shell command, a secret read, or a
  POST. That instruction does not revoke a capability, it asks the host agent
  not to use one; whether the host agent actually has that capability is up to
  the host runtime it executes in, per the next residual. We do not claim to
  detect every injection attempt, and we do not claim to remove the underlying
  capability; we claim the instructed blast radius of one is small.
- **We cannot fix the host agent's whole safety.** This repo ships prompts and
  plain JS templates that some other agent (Claude Code, Codex, Gemini CLI, an
  MCP host) actually executes. If that host agent's own sandbox, tool
  permissions, or judgment is compromised, no amount of care in this repo's
  markdown can compensate. The posture above is what this repo controls: what it
  asks the host agent to do, not what the host agent is capable of regardless.
- **Lookalike forks are possible, and the audit bar changed with v0.2.** Nothing
  stops someone from copying these files under a similar name with malicious
  edits (a new dependency slipped in, `hooks/inject-ruleset.mjs` swapped for a
  version that ignores `checkHookSafety`, a `postinstall` script added, a
  fan-out contract quietly loosened). The v0.1.0 check ("confirm `package.json`
  has no `dependencies`") no longer applies, this repo has three real runtime
  dependencies now. Install from the canonical repository only, and instead
  confirm: `package.json`'s `dependencies` match exactly `commander`, `js-yaml`,
  `zod` (nothing extra), there is still no `postinstall`/`preinstall` script,
  and `npm test` passes (which mechanically re-runs `checkHookSafety` against
  whatever `hooks/*.mjs` the fork actually ships) before trusting a fork or a
  redistribution of this suite.
- **`checkHookSafety` is a regex gate, not a guarantee.** It catches the direct,
  literal forms of a third-party import, a network/exec/spawn reference, a
  dynamic `import()`, or a stray `process.env` read. It does not parse an AST
  and cannot catch a banned token assembled at runtime via string
  concatenation. The residual mitigation is the file staying small (about 30
  lines today) and getting real human review on every PR that touches it, not
  the gate catching everything on its own.
- **Dependabot and `npm audit` cover known, published vulnerabilities only.**
  A zero-day in `commander`, `js-yaml`, or `zod` that has not yet been
  published to an advisory database would show as 0 findings today and still
  be a real risk. This is the same residual every dependency-bearing project
  carries; it did not exist under v0.1.0's empty dependency graph, and it is
  accepted, not eliminated, by declaring and monitoring the three deps above.

## How to audit in two minutes

1. `cat package.json` and confirm `dependencies` is exactly `commander`,
   `js-yaml`, `zod` (nothing extra), `devDependencies` is exactly `vitest`,
   there is still no `postinstall` or `preinstall` script, and `scripts`
   entries are limited to `test`, `sync`, and the two `bench:*` entries.
2. Read `hooks/inject-ruleset.mjs` top to bottom, it is about 30 lines, and
   confirm it imports only `node:*` built-ins, never calls `fetch`, never
   spawns a process, and reads `process.env` only for `DSTRANGE_STATE_DIR`.
3. `grep -rn "postinstall\|preinstall\|child_process\|eval(\|atob(" --include="*.js" --include="*.mjs" .` and confirm every hit is this file, a comment explaining the obfuscation or hook-safety gate, the regex literals inside `scripts/validate.mjs` that implement those gates, the one legitimate `import { execFileSync } from 'node:child_process'` in `scripts/validate.mjs` (used for a read-only `node --check` probe), or the same legitimate import in `hooks/__tests__/inject.test.mjs` (the test harness spawning the hook as a subprocess to test it, not the hook itself, `checkHookSafety` only scans files directly under `hooks/`, not `hooks/__tests__/`).
4. Read the three `templates/*.workflow.js` files top to bottom (each is under
   120 lines) and confirm every `CONTRACT` string forbids destructive shell,
   secrets/env access, and POSTing data.
5. Run `npm test` and confirm it prints `validate OK: 3 skill(s), no banned
   dashes, templates parse, hooks safe` with exit code 0, that line includes
   the `checkHookSafety` gate passing.
6. Run `npm audit` and confirm 0 vulnerabilities against the declared
   dependency graph.
7. Run `git log --oneline` and confirm you are on the canonical repository, not
   a fork with unexplained extra commits.

That is the entire audit. Nothing in this repo requires more than a text editor,
a terminal, and `node`.

## Scan results

Real verdicts only. Anything not actually run is marked pending with the exact
command to run it, never a fabricated pass.

### `npm test` (vitest suite, plus the validator: frontmatter gate, dash gate, obfuscation gate, template-parse gate, sync-drift gate, hook-safety gate)

Ran from the repo root on 2026-07-17, on `feat/v0.2`:

```
$ npm test

> d-strange@0.2.0 test
> vitest run && node scripts/validate.mjs

 RUN  v4.1.10 /Users/.../d-strange

 Test Files  2 passed (2)
      Tests  5 passed (5)

validate OK: 3 skill(s), no banned dashes, templates parse, hooks safe
```

Exit code 0. PASS. `npm test` now runs two layers where v0.1.0 ran one:
`vitest run` (5 tests across 2 files, including `hooks/__tests__/inject.test.mjs`,
which spawns the real hook and asserts on its stdout for each mode, and
`scripts/__tests__/validate-hooks.test.mjs`, which unit-tests `checkHookSafety`
directly against fixture files), then the same validator as before, now
extended with the hook-safety gate (`hooks safe` in the final line).

### `npm audit` (dependency vulnerability scan)

Ran from the repo root on 2026-07-17, on `feat/v0.2`, against the declared
dependency graph (`commander`, `js-yaml`, `zod`, `vitest`):

```
$ npm audit

found 0 vulnerabilities
```

Exit code 0. PASS. This is new as of v0.2, v0.1.0 had an empty dependency
graph and nothing for `npm audit` to check.

### `semgrep --config auto .` (SAST, ran locally)

Semgrep 1.168.0 was available on the auditing machine. Ran from the repo root on
2026-07-17, on `feat/v0.2`:

```
$ semgrep --config auto .

  Scanning 45 files tracked by git with 1074 Code rules:

  Language      Rules   Files          Origin      Rules
 ─────────────────────────────        ───────────────────
  <multilang>      47      45          Community    1074
  js              153      10
  json              4       5
  yaml             35       1

Scan Summary
Findings: 0 (0 blocking)
Rules run: 237
Targets scanned: 45
Parsed lines: ~100.0%
```

0 findings across 237 rules on 45 of this repo's 48 git-tracked files,
including the workflow YAML, `hooks/inject-ruleset.mjs`, `scripts/uninstall.mjs`,
and every other v0.2 addition. The remaining 3 tracked files are PNG logo
assets, images have no code for a SAST scanner to parse, so they are correctly
outside semgrep's target set rather than a gap. PASS. This is a fresh run
against the committed tree, not a stale result: the file count moved from 33
(v0.1.0's last recorded scan) to 45 because v0.2 added real, scannable
surface, `hooks/*.mjs`, `hooks/__tests__/*.mjs`, `scripts/uninstall.mjs`,
`scripts/__tests__/*.mjs`, `vitest.config.mjs`, `package-lock.json`, plus
documentation and reference files, not because anything was hidden from the
prior scan. This result corroborates the manual mapping table above rather
than replacing it; semgrep's ruleset is general-purpose SAST and does not have
a skill-suite-specific ruleset, so a clean run means no generic
code-injection, command-injection, unsafe-eval, or mutable-CI-action pattern
was found in the hook or dependency glue, it does not by itself certify the
fan-out contract design or replace `checkHookSafety`'s narrower, hook-specific
checks.

### `SkillGuard`, `ai-skill-scanner`, "Skill Security Auditor" (dedicated skill scanners)

**Not run. Pending, with the reason recorded honestly rather than a fabricated
verdict.**

Searching npm and GitHub for these three names surfaced a genuine, relevant
finding for this document: there is no single canonical tool behind any of
these names.

- **"SkillGuard"** resolves to at least three unrelated npm packages
  (`skillguard` by `gauravsingh1995`, `@buzzicra/skillguard`, `@skillguard/cli`
  by `lionberg`) and at least three unrelated GitHub repositories
  (`LLMSecurity/skillguard`, `obielin/skillguard`, `bossondehiggs/skillguard`),
  none with dominant download counts or an obvious claim to being "the" official
  one. This is exactly the name-squat pattern this document's own "lookalike
  forks" residual warns about, and installing a random one of them to audit a
  security-focused repo would be self-defeating.
- **"ai-skill-scanner"** does not exist on the npm registry under that exact
  name (`npm view ai-skill-scanner` returns 404). A GitHub repository
  `suchithnarayan/ai-skill-scanner` exists and, per its own description, runs
  "LLM-powered analysis by default", meaning running it as shipped would send
  this repository's contents to a third-party LLM API. That is a real
  data-exfiltration tradeoff for a security scan and is not something this audit
  will do without a maintainer first choosing and vetting a specific provider
  and endpoint.
- **"Skill Security Auditor"** does not exist under that literal name on npm.
  The closest match, `@panguard-ai/panguard-skill-auditor`, is a different
  product name from a single maintainer with no independent verification
  available in this audit.

Declining to `npx --yes` an unverified, ambiguously-named third-party package
against this repository is the correct application of this document's own
posture (do not add an unvetted execution surface). If a maintainer identifies
and vets a specific canonical tool, the commands to run it are:

```
npx --yes skillguard@<verified-version> scan .          # after confirming publisher identity
npx --yes @skillguard/cli@<verified-version> scan .      # after confirming publisher identity
```

Record the exact verdict here, with the exact command and package version used,
the next time one of these is actually run. Do not paste a verdict that was not
produced by an actual run.

### GitHub-native scanners (CodeQL, secret scanning + push protection, Dependabot, Socket)

The repository is pushed to `github.com/divisingh19988-gif/d-strange` (verified
via `git remote -v` and `git ls-remote origin`), so this is no longer the
"no remote configured" state recorded in an earlier draft of this document.
Status, stated precisely per scanner:

- **Dependabot is enabled** on the repository (Settings, Code security and
  analysis). One nuance worth stating plainly: the default branch, `main`, is
  still on the v0.1.0 `package.json` with an empty `dependencies` field as of
  this writing, the three real runtime dependencies (`commander`, `js-yaml`,
  `zod`) live on this unmerged `feat/v0.2` branch. Dependabot's dependency
  graph and alerts track the default branch, so it has nothing new to alert on
  until this branch merges into `main`. Once merged, re-verify with
  `gh api repos/<owner>/d-strange/vulnerability-alerts` (a `204` response means
  alerts are on) or the Settings UI, and confirm the dependency graph lists
  all three packages.
- Secret scanning and push protection are enabled from the repository's
  Settings, Code security and analysis page (a GitHub-native feature, not
  something this repo's code configures); unaffected by the v0.2 changes.
- CodeQL can be enabled via GitHub's default setup for the JavaScript surface
  (`templates/*.workflow.js`, `scripts/*.mjs`, `hooks/*.mjs` as of v0.2);
  `.github/workflows/skill-security.yml` in this repo runs the validator and
  an informational semgrep pass on every push and pull request, see that file
  for the exact steps. Not yet extended to run CodeQL itself.
- Socket (or an equivalent supply-chain scanner) now has a real, small
  dependency graph to evaluate once this branch merges, `commander`,
  `js-yaml`, `zod` at runtime. Not yet run; re-run this section with a real
  verdict once it is, rather than assuming a clean result.

## Disclosure

If you find a security issue in this repository, please open a private security
advisory on the repo (GitHub, Security tab, "Report a vulnerability") rather
than a public issue. Include the exact file, the exact concerning line or
pattern, and, if possible, the smallest reproduction. There is no dedicated
security email on file for this project yet; the private security advisory flow
is the disclosure path until one is added.
