You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The fifth and final agent-first CLI spec. machin-secure now collects honest,
opt-out, allow-listed usage counts — disclosed on stderr, inspectable via
`secure telemetry`, disabled by default in CI, and silent when off.
## What is sent (allow-list, spec §3.1)
tool, version, event (install|run|error), verb, os, arch, exit_class, ts
No install_id (omitting is the safer default, §3.3). No identity, no
arguments, no file paths, no data — anything not on the allow-list is
structurally absent (event.schema.json has additionalProperties: false).
## Disclosure (spec §1)
On the first run on a machine, a 4-line notice is printed to stderr:
[telemetry] machin-secure sends anonymous usage counts: tool, version,
[telemetry] os/arch, which verb ran, and whether it failed. No identity,
[telemetry] arguments or data. See `secure telemetry` for the exact payload.
[telemetry] Disable: MACHIN_SECURE_TELEMETRY=0 (or DO_NOT_TRACK=1)
Never on stdout (stdout is the data contract). Recorded once, not repeated.
## Off-switches (spec §2, checked before any network code)
MACHIN_SECURE_TELEMETRY=0 tool-specific opt-out
DO_NOT_TRACK=1 cross-vendor convention (MUST honour)
secure telemetry --off persisted opt-out
CI/GITHUB_ACTIONS/GITLAB_CI/BUILDKITE default disabled in automation
## Transport (spec §5)
Single POST over HTTPS, 2s timeout, no retry. Silent on failure — a collector
that is down is indistinguishable from one that answered. Never changes exit
code, never writes to stdout. Sends after work completes.
Endpoint: https://feedback.intrane.fr/v1/telemetry (same relay as feedback,
different path). Overridable via MACHIN_SECURE_TELEMETRY_URL.
## secure telemetry (spec §6)
Prints the exact next payload from the same code path that sends — inspectable,
not trustable. Includes enabled, reason, endpoint, next_payload, disable,
notice_shown. Supports --off (persisted opt-out).
## Events (spec §4)
install — first run on a machine, once ever (highest-value event)
run — a verb completed (exit 0 or 2 = findings, both success)
error — a verb exited non-zero (80-119 range, exit_class mapped)
At most one event per invocation. No timers, no background schedules.
## Version nudge also respects DO_NOT_TRACK and CI
The cli-update-spec nudge now checks DO_NOT_TRACK=1 and CI markers, so
DO_NOT_TRACK=1 produces zero outbound connections — verifiable with
strace -f -e trace=connect (the stranger test from spec §8).
## Conformance test (spec §8)
DO_NOT_TRACK=1 strace ... | grep connect → 0 connects
CI=1 strace ... | grep connect → 0 connects
MACHIN_SECURE_TELEMETRY=0 → telemetry off (nudge may still fire)
secure telemetry → prints real next_payload
secure telemetry --off → persisted, survives runs
first run → 4-line notice on stderr, install event
second run → no notice, run event
error (bad target) → error event, exit_class 90
Verified:
- telemetry --help: no notice (help is not usage)
- telemetry status: enabled, correct next_payload, endpoint, notice_shown
- DO_NOT_TRACK=1: 0 outbound connects (strace verified)
- CI=1: 0 outbound connects
- MACHIN_SECURE_TELEMETRY=0: disabled, reason shown
- telemetry --off: persisted, reason "disabled by config"
- first run: [telemetry] notice on stderr, stdout pure JSONL (1861 findings)
- second run: no notice
- install event sent on first run (mock server)
- run event sent on second run (mock server)
- error event sent on bad target (exit_class 90, mock server)
- scan: 1861 findings, byte-identical
- guide: telemetry in gotchas + introspection
- help-json: telemetry command + env vars
- --help: telemetry in usage + subcommands
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
g = g + "\"verdict\":[\"secure verdict --target . <id> <keep|drop> [--reason TEXT]\",\"secure verdict --target . --stdin < batch.jsonl\"],"
1043
1044
g = g + "\"suppress\":[\"secure suppress --target . --rule RULE --glob GLOB [--reason TEXT]\",\"secure suppress --target . --stdin < batch.jsonl\"],"
1044
-
g = g + "\"introspection\":[\"secure guide\",\"secure guide --human\",\"secure --help\",\"secure feedback \\\"<message>\\\" [--kind bug|idea|praise|note]\"]},"
1045
+
g = g + "\"introspection\":[\"secure guide\",\"secure guide --human\",\"secure --help\",\"secure help-json\",\"secure telemetry\",\"secure update --check\",\"secure feedback \\\"<message>\\\" [--kind bug|idea|praise|note]\"]},"
1045
1046
g = g + "\"examples\":["
1046
1047
g = g + "{\"goal\":\"scan a PR for new findings\",\"do\":[\"secure --target . --diff-base origin/main --pending --context 3\",\"# agent triages the findings\",\"secure verdict --stdin < keep_drop_batch.jsonl\"]},"
1047
1048
g = g + "{\"goal\":\"suppress repeated false positives from one rule\",\"do\":[\"secure suppress --target . --rule js-hardcoded-secret --glob '*.vue' --reason 'Vue prop bindings, not secrets'\",\"secure --target . --pending # those findings no longer appear\"]},"
@@ -1055,7 +1056,8 @@ func guide_json() (g) {
1055
1056
g = g + "\"--pending takes precedence over --show-all (even with show-all, pending excludes reviewed findings)\","
1056
1057
g = g + "\"exit codes: 0 = clean, 1 = error, 2 = high/critical findings present\","
1057
1058
g = g + "\"stdout = data (JSONL/SARIF/summary), stderr = nothing or logs — pipe-friendly\","
1058
-
g = g + "\"the scan loop is regex SAST, not taint tracking — use CodeQL for data-flow analysis (it's free for public repos)\"],"
1059
+
g = g + "\"the scan loop is regex SAST, not taint tracking — use CodeQL for data-flow analysis (it's free for public repos)\","
1060
+
g = g + "\"telemetry: anonymous usage counts (tool, version, verb, os/arch, exit_class) — no identity, args or data. Disable: MACHIN_SECURE_TELEMETRY=0 or DO_NOT_TRACK=1. Inspect: secure telemetry. Disabled in CI by default\"],"
1059
1061
g = g + "\"version\":\"" + version + "\","
1060
1062
g = g + "\"see_also\":[\"secure --help\",\"secure feedback --help\"]"
1061
1063
g = g + "}"
@@ -1215,6 +1217,7 @@ func cmd_help_json(a) {
1215
1217
h = h + "\"guide\":{\"args\":[],\"flags\":[\"--human\"],\"auth\":false},"
1216
1218
h = h + "\"feedback\":{\"args\":[\"<message>\"],\"flags\":[\"--kind bug|idea|praise|note\",\"--context TEXT\"],\"auth\":false},"
1217
1219
h = h + "\"update\":{\"args\":[],\"flags\":[\"--check\",\"--force\"],\"auth\":false},"
1220
+
h = h + "\"telemetry\":{\"args\":[],\"flags\":[\"--off\"],\"auth\":false},"
1218
1221
h = h + "\"help-json\":{\"args\":[],\"flags\":[],\"auth\":false}"
1219
1222
h = h + "},"
1220
1223
h = h + "\"exit_codes\":{"
@@ -1227,7 +1230,7 @@ func cmd_help_json(a) {
1227
1230
h = h + "\"100\":\"external/integration error (relay unreachable)\","
1228
1231
h = h + "\"110\":\"internal error / bug\""
1229
1232
h = h + "},"
1230
-
h = h + "\"env\":[\"FEEDBACK_RELAY\",\"USER\",\"MACHIN_SECURE_NO_NUDGE\"],"
1233
+
h = h + "\"env\":[\"FEEDBACK_RELAY\",\"USER\",\"MACHIN_SECURE_NO_NUDGE\",\"MACHIN_SECURE_TELEMETRY\",\"MACHIN_SECURE_TELEMETRY_URL\",\"DO_NOT_TRACK\"],"
1231
1234
h = h + "\"see_also\":[\"secure guide\",\"secure --help\"]"
// Honest, opt-out, allow-listed usage counts. No identity, no arguments, no data.
1515
+
// Disclosed on stderr before the first send. Disabled by default in CI.
1516
+
// Inspectable: `secure telemetry` prints the exact next payload.
1517
+
1518
+
var telemetry_endpoint = "https://feedback.intrane.fr/v1/telemetry"
1519
+
var current_verb = "scan"
1520
+
1521
+
// tel_config_dir returns the tool's config directory (~/.machin-secure).
1522
+
func tel_config_dir() (d) {
1523
+
home := env("HOME")
1524
+
if home == "" { home = "/tmp" }
1525
+
d = home + "/.machin-secure"
1526
+
mkdir(d)
1527
+
return d
1528
+
}
1529
+
1530
+
// tel_enabled returns true if telemetry is enabled, false if any off-switch is set.
1531
+
// Checks ALL switches before any network code (spec §2.1).
1532
+
func tel_enabled() (ok) {
1533
+
ok = false
1534
+
// DO_NOT_TRACK=1 (cross-vendor convention, MUST honour)
1535
+
dnt := env("DO_NOT_TRACK")
1536
+
if dnt == "1" { return false }
1537
+
// MACHIN_SECURE_TELEMETRY=0/false/off/no
1538
+
tel := env("MACHIN_SECURE_TELEMETRY")
1539
+
if tel == "0" || tel == "false" || tel == "off" || tel == "no" { return false }
1540
+
// persisted off flag
1541
+
cfg := tel_config_dir()
1542
+
off_kind, _, _ := stat(cfg + "/telemetry-off")
1543
+
if off_kind == 1 { return false }
1544
+
// CI is not a user — default disabled in automation (spec §2.2)
1545
+
if env("CI") != "" { return false }
1546
+
if env("CONTINUOUS_INTEGRATION") != "" { return false }
1547
+
if env("GITHUB_ACTIONS") != "" { return false }
1548
+
if env("GITLAB_CI") != "" { return false }
1549
+
if env("BUILDKITE") != "" { return false }
1550
+
return true
1551
+
}
1552
+
1553
+
// tel_notice prints the 4-line disclosure to stderr, once per machine (spec §1).
1554
+
// Uses system("printf ... >&2") because MFL has no stderr builtin.
1555
+
func tel_notice() {
1556
+
cfg := tel_config_dir()
1557
+
notice_file := cfg + "/telemetry-notice-shown"
1558
+
nk, _, _ := stat(notice_file)
1559
+
if nk == 1 { return }
1560
+
// print 4-line notice to stderr
1561
+
system("printf '%s\\n' '[telemetry] machin-secure sends anonymous usage counts: tool, version,' '[telemetry] os/arch, which verb ran, and whether it failed. No identity,' '[telemetry] arguments or data. See `secure telemetry` for the exact payload.' '[telemetry] Disable: MACHIN_SECURE_TELEMETRY=0 (or DO_NOT_TRACK=1)' >&2")
1562
+
write_file(notice_file, "1")
1563
+
}
1564
+
1565
+
// tel_payload builds the JSON payload from the allow-list (spec §3.1).
1566
+
// No install_id — omitting it is the safer default (spec §3.3).
0 commit comments