Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

Commit

A sentence is not a credential, and the g1t plugin is 0.2.10: after a colon, the redactor took the next word as a value, so on flagon-io/g1t a session's own commit message read "A path is not a secret: [redacted] redactor's…". After a colon (YAML, JSON, a header), an unquoted plain word under 16 characters is the sentence going on (prose); password: hunter2, a quoted value, a long run, and anything after = are still hidden. The server (crates/scan/src/redact.rs) and the plugin (hooks/g1t.mjs) agree. What was recorded redacted before stays so. Checked: cargo test -p g1t-scan (73; new: a sentence after a colon), -p g1t-work (214); node --test plugins/claude-code/test/hooks.test.mjs (59); scripts/plugins-release.test.mjs.

G1t-Session:
ses_01m501jq3vetja2s4a904rs068
syntaqxcommitted Parent6691441Browse files
5 files+40−50/5 viewed
+1−1
77 "name": "g1t",
88 "source": "./plugins/claude-code",
99 "description": "Work on g1t issues from Claude Code: records your sessions on g1t, loads an issue's work context, claims and hands off work, and connects g1t's MCP server.",
10− "version": "0.2.9",
10+ "version": "0.2.10",
1111 "category": "development"
1212 }
1313 ]
+27−1
8686 || value == "…"
8787 }
8888
89+/// `secret: the` in a sentence: after a colon, an unquoted plain word
90+/// short of a password's length is the sentence going on, not a value.
91+/// `password: hunter2!` (a digit, a symbol) and a long run still count;
92+/// after `=` everything does.
93+fn prose(head: &str, value: &str) -> bool {
94+ head.trim_end().ends_with(':')
95+ && !value.starts_with(['"', '\'', '`'])
96+ && value.chars().count() < 16
97+ && value.chars().all(|c| c.is_alphabetic() || matches!(c, '\'' | '’' | '-'))
98+}
99+
89100 fn shannon(text: &str) -> f64 {
90101 let mut counts = std::collections::HashMap::new();
91102 for c in text.chars() {
157168 let value = &c[2];
158169 // `token: string` in code is a type, not a token.
159170 let typed = matches!(value, "string" | "String" | "str" | "&str" | "number" | "bool" | "boolean" | "null" | "None" | "undefined" | "true" | "false" | "Option<String>");
160− if reference(value) || typed { c[0].to_owned() } else { format!("{}{REDACTED}", &c[1]) }
171+ if reference(value) || typed || prose(&c[1], value) { c[0].to_owned() } else { format!("{}{REDACTED}", &c[1]) }
161172 });
162173 let out = LONG.replace_all(&out, |c: &Captures| {
163174 if random_looking(&c[0]) { REDACTED.to_owned() } else { c[0].to_owned() }
240251 }
241252
242253 #[test]
254+ fn a_sentence_after_a_colon_is_not_a_value() {
255+ for ordinary in [
256+ "A path is not a secret: the redactor's rule read it as one",
257+ "The API key: what it is and where to keep it",
258+ "password: the one you set at sign-up",
259+ ] {
260+ assert_eq!(redact(ordinary), ordinary);
261+ }
262+ hidden("password: hunter2", "hunter2");
263+ hidden("db_password: correcthorsebatterystaple", "correcthorsebatterystaple");
264+ hidden("api_key: 'plainword'", "plainword");
265+ hidden("SECRET=plainword", "plainword");
266+ }
267+
268+ #[test]
243269 fn references_and_types_are_not_secrets() {
244270 let line = "TOKEN=$G1T_TOKEN; token: ${{ secrets.DEPLOY }}; password: process.env.PW; fn f(token: String)";
245271 assert_eq!(redact(line), line);
+1−1
11 {
22 "name": "g1t",
3− "version": "0.2.9",
3+ "version": "0.2.10",
44 "description": "Work on g1t issues from Claude Code: records your sessions on g1t, loads an issue's work context, claims and hands off work, and connects g1t's MCP server.",
55 "author": { "name": "Flagon, Inc.", "url": "https://g1t.sh" },
66 "homepage": "https://docs.g1t.sh/guides/claude-code-plugin/",
+10−1
150150 .replace(QUERY, `$1${REDACTED}`)
151151 .replace(FLAG, keep)
152152 .replace(/((?:^|\s)(?:-u|--user)\s+[^\s:]+:)\S+/g, `$1${REDACTED}`)
153− .replace(ASSIGNED, (whole, head, value) => (TYPES.has(value) ? whole : keep(whole, head, value)));
153+ .replace(ASSIGNED, (whole, head, value) => (TYPES.has(value) || prose(head, value) ? whole : keep(whole, head, value)));
154+}
155+
156+/**
157+ * `secret: the` in a sentence: after a colon, an unquoted plain word short
158+ * of a password's length is the sentence going on, not a value.
159+ * `password: hunter2!` and a long run still count; after `=` everything does.
160+ */
161+function prose(head, value) {
162+ return head.trimEnd().endsWith(":") && !/^["'`]/.test(value) && [...value].length < 16 && /^[\p{L}'’-]+$/u.test(value);
154163 }
155164
156165 /** Text a person or an agent wrote, as it is sent: credentials out, cut to `limit`. */
+1−1
330330 assert.ok(!out.includes(secret), `${secret} in ${out}`);
331331 assert.ok(out.includes("[redacted]"), out);
332332 }
333− for (const kept of ["TOKEN=$G1T_TOKEN", "token: ${{ secrets.DEPLOY }}", "fn f(token: String)", "npm_config_user_agent", "git show 58def4040e93a1dcda5f388f55360f3cd46cd97e"]) {
333+ for (const kept of ["A path is not a secret: the rule read it as one", "password: the one you set", "TOKEN=$G1T_TOKEN", "token: ${{ secrets.DEPLOY }}", "fn f(token: String)", "npm_config_user_agent", "git show 58def4040e93a1dcda5f388f55360f3cd46cd97e"]) {
334334 assert.equal(redact(kept), kept);
335335 }
336336 });

Comments

Sign in to comment.