With the ruflo CLI option on npx, a confirmed action ran a cold online npx under the 90 s default and aborted with 'still running after 90000ms'. Resolve ruflo on PATH, a project-local bin or the cached npx copy first; give a cold download a 300 s limit and a first-run label; turn a timeout or ENOTCACHED into an error naming the fix. Console 0.40.1; ADR-407 update.
Co-Authored-By: RuFlo <ruv@ruv.net>
Default-off Settings rows: a read-only claude -p consult (asked first, exact argv, turn budget, mission spend cap) before a plan locks, when the same check fails twice in a row, and before done; a third failure pauses the mission. Honest about not being the in-session advisor tool. Console 0.40.0.
Co-Authored-By: RuFlo <ruv@ruv.net>
The mission goal was cut at 500 characters and drawn on one clipped line; the host Input is one line and hides the rest. A live mirror
under every field shows all lines, views draw text in full with an explicit marker, limits are checked before sending with the exact
count, and every silent slice/plain cap on typed text is removed or made a counted refusal. console 0.39.1.
Co-Authored-By: RuFlo <ruv@ruv.net>
One dependency-free policy (canonical in ruflo-mods, byte-identical copies in console,
swarm and protector, checked by scripts/sync-toast-policy.mjs and each smoke.sh): level
prefixes, one washed line of at most 120 characters, 60 s de-duplication, four toasts a
minute per source with errors held and counted, the person's all/important/off setting and
per-source mute (console Settings, mirrored to a file the others read), and a digest of
every toast kept for the console's Events page. All call sites migrated; new toasts for a
mission that finishes or fails and a swarm subagent that fails or a loop that is stuck.
Versions: console 0.37.0, swarm 0.3.4, protector 0.1.1, mods 0.3.16.
Co-Authored-By: RuFlo <ruv@ruv.net>
- plugin.json declares eventsPersist (default true); version 0.36.0 in plugin.json, version.ts, smoke.sh
- data/append-argv.ts: one dd argv (bs=1M iflag=fullblock) shared by the Events store and the autopilot journal; real-dd 8-writer test fails on the old argv
- history decoders and Events/Timeline displays use #3874's timeOf/countOf/isoOf, so a hostile time or count cannot throw
- cli init: .claude-flow/.gitignore template lists console/
- ADR-474 updated to the reviewed behaviour and the open low items
Co-Authored-By: RuFlo <ruv@ruv.net>
Conflict in views/manage.ts: #3874 (isoOf) edited eventsView, which the Events work moved to views/events.ts; the move wins and the isoOf clamp is applied at every Events/Timeline site instead.
Co-Authored-By: RuFlo <ruv@ruv.net>
$'\x2f' and $'\x72m' name the root and rm without any expansion, and a
\' inside $'...' does not close the string. The scanner now decodes ANSI-C
escapes (octal, \x, \u/\U, \c, the single-letter set, NUL ends the string)
in all four copies, and the parity table gains wrapper, flag-spelling,
benign look-alike and documented-limit rows.
Co-Authored-By: RuFlo <ruv@ruv.net>
The scan reports hook patterns as written, so tool.*, prompt.*, agent.* or
!tool.describe hooked every risky event without matching a RISKY_EVENTS key.
Co-Authored-By: RuFlo <ruv@ruv.net>
refuse-risky admitted a user-tier mod that ran host commands through
$.process.spawn (process.run was listed) or through any MCP tool via
$.mcp.call, and one that steered every prompt (prompt.submit) or every
subagent (agent.spawn). Observe told the person "no risky calls or hooks".
The risk sets now cover every call in the engine's $ API that reaches
outside the session, and the two hooks from #3787. Observe-mode logging is
keyed on provenance plus the risk set, so a hot reload that gains a risky
call or hook is named again ("and every reload"), and the same risk is not
repeated.
(cherry picked from commit 566bfdc8a20409f763d312e1ae34bcd0fc6c0515)
Adversarial review follow-up: the list test only covered the conditions it
named, and the doc comment claimed smoke.sh guarded drift (it checks names,
not conditions). The matrix test calls register() with a recording on() for
256 option combinations and compares.
(cherry picked from commit d857b4d136)
registeredEvents() listed them as always registered, so with default options
the probe counted session.measure as "fired" with no ruflo hook running and
listed agent.spawn as "never fired" although the mod never hooked it (and
plugin.register under modTrust: off).
(cherry picked from commit 3a146c08dc)
Projects initialised before hook-handler.cjs (e.g. settings-generator at
d7212d25b wrote `npx claude-flow@v3alpha hooks route --task "$PROMPT"`) run
the CLI's own hooks command, which never reads RUFLO_MODS_OWNS. The mod saw
no classic route/post-edit there, owned both, and every prompt was routed
twice (the CLI still prints its block with the handshake set, verified on
3.52.0). A settings hook running claude-flow / @claude-flow/cli / ruflo
`hooks route|post-edit` now makes the mod stand down for that event.
On Windows the generated hook falls back to
%USERPROFILE%\.claude\helpers\hook-handler.cjs; helperHonours now checks it
beside the project root and $HOME.
(cherry picked from commit 4dec48e9f5)
Fourth review (differential vs main over 154k inputs): two secret shapes
main caught were missed after the earlier tightening - a Google key glued to
"-suffix" (the trailing lookahead rejected it; \b now also ends it) and a
JWT after a '-' (the new lookbehind; the rule now starts at the first eyJ
after a '-' or the run start, still once per run and linear). A later window
that starts mid-line gets a lead character so ^ cannot treat a benign
"system:" there as a role tag. Inbound and outbound now build the windows
once and pass them to scanWindows(). Two tests (each fails on the previous
head); corpus unchanged.
(cherry picked from commit 5e9cec209c)
Third review: stripping every default-ignorable character and NFKC folding
joined what followed to the word before it, so a leading \b or lookbehind
failed: "x" + soft hyphen + "Ignore all previous instructions", a circled
digit before AKIA..., and a zero-width space before an SSN all passed, where
main caught them (129 of 1,093 sampled inputs).
windows() now screens three readings: the folded one, main's literal one
(only its original invisible set removed) and a spaced one (invisible
characters become spaces), each whitespace-collapsed and windowed, so cost
stays linear. The role-tag rule is back to main's \s* (linear after the
collapse; it caught "system\n:"), and the key rule accepts doubled or mixed
separators again (my--token=). One test pins seven of these cases; it fails
on the previous head.
(cherry picked from commit 000f677c36)
Second review: once the whole text is screened, two older rules that were
only ever run on a 20k prefix became the cost: `fake role tags` (^\s* with
m) is quadratic on runs of newlines (1 MB of "\n": 16.8 s) and `jwt` (\beyJ)
restarts at every '-' ("-eyJ" x 1 MB: 8.2 s). Unbounded \s in shell pipe,
key assignment and bearer also let padding carry a match across a window.
- bare() collapses each whitespace run to one space or newline before
windowing (linear; screening only tests text, never returns it).
- jwt starts once per token run; the anchored role-tag half uses [ \t].
- key assignment accepts a CLI flag prefix again (--password=, --token=),
which main caught.
Three tests pin these (1 MB of blank lines and "-eyJ" under 1.5 s, a pipe
padded with 1.1k-25k spaces at window boundaries, CLI flags); each fails on
the previous head.
(cherry picked from commit 2ffd534404)
Adversarial review of the previous commit:
- the suffix group after the keyword backtracked against it: 1 MB of
'token_' took 6.4 s through scan() (10 MB: 71 s), and with the 20k cap gone
a peer controlled the input. The keyword must now end the name, optionally
followed by _key / _access_key; 1 MB of 'token_' screens in milliseconds.
- the same group flagged config names that only start with a keyword
(TOKEN_URL=, MAX_TOKENS=, session_token_ttl=, CREDENTIALS_FILE=); those
pass again, and an all-digit value is no longer a credential.
- a later window began mid-word, so its lookbehind, \b and ^ saw a false
start of text ('q' * 18976 + 'secret=...' was refused, unpadded it passes).
Later windows now start after whitespace within 256 characters; the 1,024
overlap still covers every rule.
Three tests pin these; each fails on the previous head.
(cherry picked from commit 60a5f05340)
The shared screen behind deliveryScreen, compactCarry and guidance safeText:
- scanned only the first 20,000 characters, so padding pushed an injection
or a secret past it. It now scans the whole text in overlapping 20k
windows (1k overlap, far longer than any rule spans): still linear.
- stripped a fixed list of invisible characters. It now strips every
default-ignorable code point (soft hyphen, CGJ, Mongolian VS, variation
selectors, bidi isolates, tags) plus C0/C1 and line/paragraph separators,
then NFKC-folds fullwidth forms.
- missed GITHUB_TOKEN= / aws_secret_access_key = / "client_secret": (the
keyword inside an _ or - separated name), PGP armored private keys, Google
keys ending in '-', and Slack xapp- tokens.
delivery/screen.ts reuses the shared windows instead of its own copy of the
invisible set and the 20k slice. The 50/50 acceptance corpus is unchanged
(50/50 caught, 1/50 benign flagged, as before).
(cherry picked from commit b1cc41c887)
Re-review follow-ups: the engine contract says the process also goes on
under another session after a resume (no session.start), so the rollup
resets on reason resume as on clear (new test). The cost reset stays on
clear only until /resume cost behaviour is measured live. The cost reset
now redraws the status line, which otherwise showed the old rung until the
next prompt.
(cherry picked from commit 927799ae24)
Adversarial review: the engine contract (2.1.289 types) says session.start
fires once per process or fresh load, "never /clear"; /clear is seen as
session.end with reason clear and no session.start follows. The previous
commit reset on session.start, so it never ran on a real /clear, and its
tests faked one with an `as never` cast.
- rollup: after writing the ending session's record, a clear resets the
counters and the written flag, so the next session gets its own record.
- cost: a session.end matcher on reason clear restarts the ladder and the
hard stop. Registered after the rollup, which records the rung first. Any
other end reason keeps the hard stop (new test).
- tests replay the real sequence: session.end(clear), work, session.end.
Live (2.1.289, --plugin-dir, tmux): prompt, /clear, prompt, /exit now writes
two ledger records; main writes one.
(cherry picked from commit 3630744aea)
/clear ends a session and starts the next in the same process without
re-running register, so the process-scoped state carried over:
- sessionRollup set `written` once and never reset it, so every session after
the first /clear went unrecorded (measured live on 2.1.289), and its
counters accumulated. The rollup now resets on session.start and records
routed/tightened as this session's share.
- the cost ladder kept the previous session's announced rung and HARD_STOP
level, though the engine counts the new session's cost from zero
($0.07 -> $0.06 across /clear, measured live). The ladder and the hard stop
now start over at session.start; within a session a rung is still told once.
The engine refuses a second matcher-less session.start, so the cost hook uses
the same match-everything surface matcher as the rollup hook.
(cherry picked from commit 0f9f1b99f2)
Adversarial review follow-ups in the same class:
- flushObservations read and parsed in one try and applied isMissing to the
parse error, so a corrupt queue quoting ENOENT was overwritten, against its
own "corrupt bytes are never replaced". Read and parse are separate now.
- isMissing matched ENOENT anywhere in the message, including the quoted
path (a project under .../ENOENT-x/). It now takes the path and ignores
it; cachedFile and the queue pass theirs.
(cherry picked from commit 5044d007f3)
cachedFile applied isMissing() to parse() exceptions as well as to the
fs.read rejection. JSON.parse and parseProjection both put the file's own
text in their message, so an enforced policy projection containing "ENOENT"
(torn JSON, a stray log line, version: "ENOENT") read as "no policy" and the
guard allowed what the rule denies. Only the read may say absent now; every
parse failure is an error, so the guard fails closed as ADR-450 T10 says.
(cherry picked from commit 5a0a5a4cf9)
Real-bash oracle (bwrap, read-only root, logging fake rm; positive controls
detected) over a 63,696-command differential between main and this branch:
- a substitution inside double quotes is now dropped from the word, as its
empty output is, so "`true`"-rf reads as -rf; a placeholder is kept only
when a # follows, so bash -c "`true`#x; rm -rf /" is still read as one
word and caught;
- 1,260 commands main denies and this branch allows: 0 really delete
(bash parses each differently than main assumed);
- of 1,500 sampled commands this branch allows with a root operand, the
201 that really delete are all $(...) substitutions (stated follow-up, 189)
or $- expansions inside sh -c (12); main misses every one too.
Three parity rows added (309 pass). All four copies alike; the generator's
String.raw source keeps no backtick in comments.
(cherry picked from commit 4f2ac8ba28)