- Reject unparseable or empty Node.js version output before major version check
- Define minimumNodeMajor constant to eliminate duplicate version numbers
- Build child test environments immutably without in-place mutation
- Add regression test for unparseable Node.js version output
- Check for Node.js presence and major version (>= 18) before invoking installer
- Check for npm presence when node_modules is missing during git-clone installs
- Use [Console]::Error.WriteLine to ensure clean stderr output and preserve exit codes
- Add regression tests in tests/scripts/install-ps1.test.js covering missing and outdated Node.js environments
Addresses the review on #3039. Four findings, all of them real.
Skills-first (Greptile P2). AGENTS.md says commands/ exists for compatibility and
the direction is skills-first, so the workflow now lives in skills/token-card
and commands/token-card.md is a thin shim pointing at it — 70 lines down to 28.
Unpinned remote code (CodeRabbit major, Greptile P1, CWE-494). The workflow ran
`npx -y @tokenchit/cli@latest` and handed whatever the registry currently serves
read access to local session logs. It is pinned to 0.7.0, the skill says to raise
the pin knowingly, gives the `npm view … dist.integrity` check, and step 1 is now
an explicit confirmation before the first download on a machine.
The "no network request" claim was false as written (both bots). `sync` sends
nothing, but the npx invocation around it is a registry download, and the
sentence read as though the whole step were offline. It now separates the two.
`--dry-run` wrote a file (Greptile P2). Step 1 created `.tokenchit.json` via
`init` before `sync --dry-run` ran, so a documented no-op mutated the tree.
Initialisation is skipped on a dry run and reported as prospective instead.
The README offer was unreachable (Greptile P2). "Stop after step 3" sat directly
below step 4, so the step that puts the card in the README — the whole point —
could never run. The boundary moved to after step 4; publishing still requires
being asked for by name.
Wiring: agent.yaml skills list, manifests/install-modules.json under
operator-workflows (beside cost-tracking and ecc-tools-cost-audit), and
package.json `files`, which the npm-publish-surface test requires to track the
module graph. `yarn install --mode=update-lockfile` leaves yarn.lock byte
identical, so nothing to commit there. docs/tr/AGENTS.md carries two skill counts
the catalog sync does not reach, both 286 -> 287.
npm test: 4396 passed, 0 failed. markdownlint clean on both files.
Greptile's review asked for committed coverage and installer parity; this
makes the hooks installable through ECC's own machinery instead of a
copy-paste doc:
- manifests/install-modules.json: new module hermes-hooks (kind hooks,
targets hermes + openclaw, paths hooks/hermes + docs/HERMES-HOOKS.md,
light/stable, defaultInstall). Disjoint from hooks-runtime, which stays
Claude/Cursor/OpenCode/CodeBuddy — the Hermes target gets hooks support
without inheriting the Claude-format hook graph.
- manifests/install-profiles.json: full profile gains hermes-hooks (validator
requires full to include every module).
- docs/HERMES-HOOKS.md: install section now leads with
./install.sh --target hermes --modules hermes-hooks --enable-hooks; the
manual block registers from the landed ~/.hermes/hooks/hermes/ paths. The
installer never edits config.yaml — registration stays operator-owned.
- tests/hermes-hooks.test.js: installer-integration case (14 total) drives
the real installer into a throwaway HOME and proves the LANDED guard
blocks an eslint.config edit.
Validated: install-manifests (37 modules), no-personal-paths, markdownlint,
14/14 tests. E2E on a throwaway HOME: module lands both scripts + doc and
the landed guard emits a block decision.
Greptile + CodeRabbit review of #3019:
- config-protection: compare basenames case-insensitively (macOS/Windows
resolve ESLINT.CONFIG.JS to the protected eslint.config.js) and use lstat
so a dangling symlink to a protected name still blocks (os.path.exists
follows the link and returns False).
- Both hooks: fail-open stays, silence goes — every swallowed path now
writes a one-line stderr diagnostic.
- config-protection: sys.stdout.write instead of print() per repo Python
style; block decisions carry a trailing newline.
- New tests/hermes-hooks.test.js: 13 cases through the real scripts via
spawnSync — block (all names, case variants, dangling symlink, edits[],
per-tool), allow (normal source, new config, non-edit tools), fail-open
with diagnostics (malformed payload, missing path, unreadable file).
- docs: absolute-path copy-paste block via $HOME (CodeRabbit: /Users/YOU
placeholders were unverifiable), self-contained block-case proof that
creates its target, pointer to the committed suite.
Hermes ships its own shell-hook system (hooks: block in config.yaml,
Claude-compatible JSON payloads, per-tool matchers, consent allowlist),
and the hermes install target deliberately ships no hook runtime — so
Hermes operators get rules/skills/commands with zero hooks. This ports
the two harness-independent ECC hooks as self-contained Python scripts
with no ECC runtime dependency:
- config-protection: blocks edits to existing lint/format configs and
ratchet baselines (pre_tool_call)
- check-console-log: warns on console.log() in edited TS/JS (post_tool_call)
Includes install, consent, and verification steps in docs/HERMES-HOOKS.md.
The Turkish translations of the longform, security, and shortform guides
linked images with `../assets/...`, which resolves to a non-existent
`docs/assets/` directory, so every embedded image rendered as broken.
Assets live at the repository root (`assets/`), so from `docs/tr/` the
correct relative prefix is `../../assets/`. This matches the canonical
root guides (`./assets/`) and the Spanish (`docs/es/`) and Simplified
Chinese (`docs/zh-CN/`) translations, which already use `../../assets/`.
Fixes 26 image/asset links across the three guides.
Use literal matching for the forbidden documentation endpoint, pin lifecycle mode/content assertions to one descriptor, and inject parent races through the staged descriptor without forwarding arbitrary file-creation flags.
Close fixture-owned descriptors when Windows blocks a directory rename before open returns. Assert OS rejection preserves both settings files and releases staging handles. Move the rename-boundary injection after descriptor close so Windows exercises parent-identity validation without skipping race coverage.
Resolve#2957 using the verified MCP reference memory package, native session scheduling, supported CLI invocation and documented computer-use integration. Incorporates the corrective direction from #2977 and #2958, including package-version pinning and regression checks for executable examples.
Co-authored-by: ilkmajans-cpu <ilkmajans-cpu@users.noreply.github.com>
Co-authored-by: kavish-19 <63698788+kavish-19@users.noreply.github.com>
Generalize PR #2981 ownership protection to every managed target. Reject mismatched target state and preserve files that appear during writes or failed-install checkpoints. Keep prior hashes for managed files a failed attempt never writes.
Integrate PR #2980 preview wording and global dry-run propagation, with PR #2956 fail-closed environment validation and CLI/legacy regression coverage.
Fixes#2964. Fixes#2952.
Co-authored-by: ilkmajans-cpu <ilkmajans-cpu@users.noreply.github.com>
Co-authored-by: wellkilo <wellkilo@foxmail.com>
Reject directory replacement after temporary file creation or staging, preserve unrelated files during cleanup, and retry settings edits observed before the final rename. Add three regression tests for the review findings.
Address #2921 and complete the segment-anchoring direction in #2979. Preserve explicit absolute exemptions while denying accidental matches in unrelated projects.
Addresses Greptile's review on #2994:
- missing-dependency.js no longer hardcodes a second copy of the four
runtime dependency versions; it reads them from package.json's
dependencies field instead, so the two can't silently drift apart.
describeMissingDependencyError() still recognizes a tracked
dependency even if package.json can't be read for some reason,
just without a version-pinned install command in that case.
- The regression test now asserts no ancestor directory of its
temp fixture has a node_modules, so a stray one wouldn't let
Node resolve ajv/sql.js from there and mask what the test is
actually meant to exercise. Also copies package.json into the
fixture, matching a real plugin-marketplace git clone and what
the version-lookup above now needs.
install-plan.js and install-apply.js both require ./lib/install/config at
load time, and that module required ajv unconditionally at the top of the
file even though ajv is only actually used when validating an
ecc-install.json. When ECC is installed via the Claude Code plugin
marketplace, the marketplace directory is a bare git clone with no
node_modules, so requiring ajv crashes commands like --list-profiles that
never touch install-config validation at all.
Same root cause in scripts/lib/control-pane/state.js: sql.js and
@iarna/toml were required at module scope even though they are only used
inside openSqlDatabase() and readTomlConfig(), so control-pane.js --help
crashed too.
Make both requires lazy so they only load when the feature that actually
needs them runs. For the case where ajv/sql.js/js-yaml/@iarna-toml is
genuinely needed and still missing, add a small helper that turns the raw
MODULE_NOT_FOUND into an actionable message naming the package and the
install command, instead of a stack trace (install-apply.js) or, worse, an
unhandled crash with a usage banner tacked on that reads like a bad
argument (install-plan.js, control-pane.js). Applied the same helper to
memory-mcp.mjs, where ajv is genuinely load-bearing (it compiles every MCP
tool's JSON schema up front) so it can't be made lazy the same way.
Added a regression test that copies just scripts/, schemas/, and
manifests/ into a directory with no node_modules anywhere above it in the
filesystem, which reproduces the plugin-marketplace install exactly, and
asserts install-plan.js and control-pane.js still work.
Forward-port #2911 for #2909 and exercise the real adapter lifecycle with a recorded process boundary, including unavailable Node and invalid overrides.
Co-authored-by: DavidHLP <lysf15520112973@163.com>
Forward-port #2925 for #2924 and verify ASCII and multibyte over-limit input suppression. Supersedes the overlapping direct-entrypoint fix in #2978.
Co-authored-by: jackie-cqz <2557911191@qq.com>
Named in review on #2829: `dd bs=1M if=/dev/zero of=/dev/sda` is the same
class as the reversed-operand case and is denied by the token-based check,
but nothing pinned it.
The "does not gate as destructive" cases guarded the decision behind
`if (output && output.hookSpecificOutput)`, so a crashed or silent hook made
parseOutput return null and the test passed having asserted nothing.
Assert the exit code and that output parsed first, then branch: a decision
object must not be a Destructive deny, and pass-through must echo the input
back — the same shape the existing retry case already checks.
Raised in review on #2829.
Review on #2829 found the regex fix widened a false positive: matching
`\bdd\s+if=` against the whole flattened line gated `echo dd if=/dev/zero`
and `grep dd if=/dev/zero file`, neither of which runs dd. That class was
already present before — `echo dd if=x` matched the old arm too — but the
boundary fix extended it to the slash and dot spellings, so the arm now
decides on text position rather than on what is being executed.
dd moves to isDestructiveDd(tokens), next to isDestructiveRm and
isDestructiveGit, and DESTRUCTIVE_SQL_DD goes back to SQL only. The
per-segment loop already tokenizes every executable body, so the check runs
where the command word is known.
This resolves four things the text match could not:
dd if=/dev/zero of=/dev/sda was allowed -> denied (the reported bug)
sudo dd if=/dev/zero was allowed -> denied
dd of=/dev/sda if=/dev/zero was allowed -> denied (operands are order-free)
echo dd if=x was denied -> allowed (pre-existing false positive)
Leading sudo/doas/env, their flags, and VAR=value assignment prefixes are
skipped so a wrapped invocation still resolves to dd; flags are only skipped
once a wrapper has been seen, so the scan cannot walk into an unrelated
command's arguments.
Tests: 6 fail on upstream main, 155 pass with this change.
Refs #2642
DESTRUCTIVE_SQL_DD shared one trailing \b across every alternation arm.
`dd\s+if=` ends in `=`, and a \b after a non-word character only holds when
the NEXT character is a word character, so the arm matched `dd if=x` and
missed every path starting with `/`, `.` or a quote:
dd if=/dev/zero of=/dev/sda allowed
dd if=./disk.img of=/dev/sdb allowed
dd if="/dev/zero" of=/dev/sda allowed
The word-boundary suffix now applies only to the arms that end in a word
character. The split is what keeps the widening bounded: dropping the
trailing \b outright would let `truncate` match `truncated`, and dropping the
leading \b would let `dd if=` match inside `add if=`. Both are covered.
This is the half of #2642 that survived the structural findGitSubcommand()
parser, which already handles `git checkout -- .`.
Not addressed here: `dd of=/dev/sda if=/dev/zero` with the operands reversed
is still allowed, before and after, because the pattern requires `if=`
immediately after `dd`. That is a different defect from the boundary bug and
widening a P0 gate's pattern shape is a maintainer call.
Refs #2642