Two fail-closed checks in security-engine.mjs convert (decision 224), both "not measured" (exit 2, the reason on stderr, nothing on stdout): - the stamp's findings.total is not a non-negative integer, or the JSONL holds another number of records (a JSONL that lost the only BLOCKER line converted clean before; part 3 verifier, F3); - the stamp's scope is not an empty list: a scan limited by --scope, an absent scope or a non-list scope (Claude Security 0.12.0 writes scope: [] for an unscoped scan; F4). The real Claude Security 0.12.0 probe report still converts: exit 0, one BLOCKER, two below_threshold. Decision 223, text only: the module header and SECURITY.md keep the head/base check and say it is a trace, not a signature: code the reviewed run executed can write a report folder of its own (convert reads the matching one whose name sorts last) or delete the real one first (F2, a known issue). /trekreview's not-measured list names the two new reasons. The test helper writes the real stamp shape by default (findings.total = the records in the JSONL written, scope: []). New tests for behaviour that had none (F6): no fallback from an unverified newest match, a two-stamp folder passed over, a symlinked stamp, JSONL or folder refused, a malformed or absent scan_prefix, an unrecorded empty_diff or empty_scope. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6.9 KiB
Security Policy — trekplan
Reporting a vulnerability
Report privately to security@fromaitochitta.com — do not open a public issue. There is no SLA — this is a solo-maintained plugin — but acknowledged reports are usually triaged within 7 days.
If you already have an account on the Forgejo instance, you may instead
open a private issue on the repo, tagged security:
Supported versions
Only the current minor version receives security fixes. When v3.2.0 ships, v3.1.x stops receiving patches. Pin to the latest minor and update on the next bump.
| Version | Supported |
|---|---|
| 3.1.x | Yes |
| 3.0.x | No (upgrade to 3.1.x) |
| < 3.0 | No |
Scope
The plugin's security posture covers:
Plugin-owned hooks (hooks/scripts/)
| Hook | Trigger | Purpose |
|---|---|---|
pre-bash-executor.mjs |
PreToolUse for Bash |
BLOCKs known-dangerous shell patterns; WARNs on suspicious ones; answers ask before merge into main, push, tag/publish and destructive git (P12); fails open on parse errors |
pre-write-executor.mjs |
PreToolUse for Write, Edit, NotebookEdit |
BLOCKs writes to .git/hooks/, ~/.ssh/, .env, and other sensitive paths, to Voyage's own state in the git directory (voyage/), and to test files a step has locked (P9); fails closed when it cannot read the target path or the lock, or cannot load the lock code |
pre-plan-gate.mjs |
PreToolUse for ExitPlanMode |
Runs Voyage's plan gate on a Voyage plan (it carries plan_version) and denies one the gate refuses; asks with "not measured" when the gate cannot run; other plans pass (P3) |
post-approval-log.mjs |
PostToolUse for Bash, ExitPlanMode |
Writes an approved merge/push to main, tag/publish, or Voyage plan to the approval log; says "not logged" when it cannot |
post-edit-check.mjs |
PostToolUse for Edit, Write |
Runs the target repo's own formatter and linter on the edited file, check-only, while Voyage executes a plan there or the repo has switched it on (P7) |
stop-test-lock.mjs |
Stop |
Refuses "done" while the session's step holds its test lock; lets the stop through when stop_hook_active is set (P9) |
pre-compact-flush.mjs |
PreCompact |
Flushes progress.json from git history before compaction (P0 drift fix); read-only beyond progress.json |
session-title.mjs (planned, F9) |
UserPromptSubmit |
Sets session title voyage:<command>:<slug> for telling parallel sessions apart |
All hooks are zero-dependency Node.js (.mjs) scripts. Most are designed
to fail open — a hook crash never blocks the user's work. The
exceptions are stated per hook above: the write guard fails closed when it
cannot read a path or a test lock, and the plan gate asks rather than
passes when it cannot run. No hook runs a model. Hooks never write to user
files outside their declared scope; the test lock and the post-edit log
live in the repository's git directory (<git dir>/voyage/), where they
cannot be committed.
Precondition: mods
Voyage's guards are deterministic only when no installed mod handles tool.check.
A Claude Code mod that handles tool.check answers after the permission
rules and the PreToolUse hooks have decided, and its answer can replace
theirs: it can approve a call a plugin hook blocked or asked about, and
outside managed settings and Team/Enterprise plans it can approve a call a
deny rule refuses (code.claude.com/docs/en/permissions, § Extend
permissions with hooks). Voyage's hooks cannot tell whether such a mod is installed:
no hook input names the loaded mods. /plugin shows them to a person
(N mods active · …). Voyage itself ships no mods.
Prompt-level denylist (commands/trekexecute.md)
The execute command embeds a denylist that takes effect even in a session where a hook does not fire. This is layer 2 of the three-layer defense-in-depth model (README, Security hardening) and protects against plan-injected destructive commands.
Validators (lib/validators/*.mjs)
Read-only. Never write to user files. Used both by hooks and by command phases to detect malformed artifacts before they propagate.
The review's security engine (/trekreview)
/trekreview hands security review of a delivery to one engine and never
reports a 0 it did not measure. Claude Security is recommended: Anthropic's
plugin, with a proprietary licence, so Voyage calls it by name
(claude-security:claude-security, scan changes only, after its own cost
question to the operator) and it is never a dependency in Voyage's manifest.
Without it, Claude Code's /security-review is used. With neither, security
is "not measured" and the verdict gate exits 2 (never green).
lib/review/security-engine.mjs chooses the engine and converts Claude
Security's report (CLAUDE-SECURITY-<ts>/, written by Claude Security in the
repo root behind its own .gitignore) as data: nothing in it is run or
followed, and a stamp counts only when it names exactly the reviewed head and
base. That check is a trace, not a signature (a known issue): code the
reviewed run executed can write a report folder of its own, which convert
reads when its name sorts last among the matching ones, or delete the real
report before convert reads it, and Voyage does not detect either. A
Claude Security finding blocks only at confidence high; the rest are listed
under Declined to Judge and keep the review from ALLOW. Neither engine
edits code.
Out of scope
- Opt-in upstream architect step. Any external producer of
architecture/overview.mdships its own security posture. The architecture-discovery validator in this plugin treatsarchitecture/overview.mdas an external contract (drift-WARN, never drift-FAIL). - LLM output content. The plugin validates artifact shape, not
artifact truthfulness. A plan that passes
plan-validator --strictmay still contain hallucinated file paths or unsafe commands; that is whypre-bash-executorexists. - The Claude Code CLI itself. Report Claude Code vulnerabilities to Anthropic via https://github.com/anthropics/claude-code/issues.
Hardening recommendations
For fork-ers handling untrusted task briefs or plans:
- Set
disableSkillShellExecution: truein~/.claude/settings.json(CC v2.1.91+) to prevent Skills from invoking arbitrary shell. - Run plan validation in
--strictmode before any execute:node ${CLAUDE_PLUGIN_ROOT}/lib/validators/plan-validator.mjs --strict plan.md - Review the plan-critic adversarial output before approving plans from external sources — semantic rubric (rule #7) catches deferred decisions that an attacker could exploit.
- Pin a CC version floor. v3.1.0 of this plugin assumes CC ≥
2.1.85 for the
if-field on hooks; older CC silently ignores the field, weakening the scoping.
Past advisories
None as of v3.1.0. This section will list CVE-style entries if any are discovered.