Skip to content

Commit efc19e6

Browse files
authored
Merge pull request #6 from CodeAnt-AI/feat/set-telemetry
secret hooks
2 parents 1fc81c7 + 537dbf0 commit efc19e6

File tree

7 files changed

+162
-9
lines changed

7 files changed

+162
-9
lines changed

changelog.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Changelog
22

3+
## [0.3.9] - 04/04/2026
4+
- Secrets bypass
5+
36
## [0.3.8] - 03/04/2026
47
- Graceful handling of review
58

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codeant-cli",
3-
"version": "0.3.8",
3+
"version": "0.3.9",
44
"description": "Code review CLI tool",
55
"type": "module",
66
"bin": {

src/commands/secrets.js

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@ import { useState, useEffect } from 'react';
22
import { useApp } from 'ink';
33
import SecretsApiHelper from '../utils/secretsApiHelper.js';
44
import { detectSecrets } from '../utils/secretsDetector.js';
5+
import { fetchApi } from '../utils/fetchApi.js';
6+
import { detectRemote, detectRepoName, detectBaseUrl } from '../scm/index.js';
57
import {
68
renderInitializing,
79
renderFetchingDiff,
810
renderScanning,
911
renderNoFiles,
1012
renderError,
1113
renderDone,
14+
renderBypassPrompt,
1215
} from '../components/SecretsUI.js';
1316

14-
export default function Secrets({ scanType = 'all', include = [], exclude = [], lastNCommits = 1, baseBranch = null, baseCommit = null }) {
17+
export default function Secrets({ scanType = 'all', include = [], exclude = [], lastNCommits = 1, baseBranch = null, baseCommit = null, hook = false }) {
1518
const { exit } = useApp();
1619
const [status, setStatus] = useState('initializing');
1720
const [secrets, setSecrets] = useState([]);
@@ -71,13 +74,65 @@ export default function Secrets({ scanType = 'all', include = [], exclude = [],
7174
return () => { cancelled = true; };
7275
}, [scanType, include, exclude, lastNCommits, baseBranch, baseCommit]);
7376

77+
// Build secrets payload for API calls
78+
function buildSecretsPayload() {
79+
return secrets.flatMap(file =>
80+
file.secrets.map(s => ({ type: s.type, file_path: file.file_path, line_number: s.line_number }))
81+
);
82+
}
83+
84+
// Build remote info using existing scm detection
85+
function getRemoteBody() {
86+
const service = detectRemote();
87+
const repo = detectRepoName();
88+
if (!service || !repo) return null;
89+
return { service, repo, base_url: detectBaseUrl() };
90+
}
91+
92+
// Fire-and-forget block event + exit 1
93+
function reportBlockAndExit() {
94+
const remote = getRemoteBody();
95+
if (remote) {
96+
fetchApi('/extension/push-protection/event', 'POST', {
97+
...remote,
98+
secrets: buildSecretsPayload(),
99+
}).catch(() => {});
100+
}
101+
setTimeout(() => { process.exitCode = 1; exit(new Error('Secrets detected')); }, 100);
102+
}
103+
104+
// Handle bypass selection
105+
async function handleBypassSelect(reason) {
106+
if (reason === 'cancel') {
107+
reportBlockAndExit();
108+
return;
109+
}
110+
const remote = getRemoteBody();
111+
if (remote) {
112+
try {
113+
await fetchApi('/extension/push-protection/bypass', 'POST', {
114+
...remote,
115+
secrets: buildSecretsPayload(),
116+
reason: reason.startsWith('other:') ? 'other' : reason,
117+
custom_reason: reason.startsWith('other:') ? reason.slice(6) : undefined,
118+
});
119+
} catch {
120+
reportBlockAndExit();
121+
return;
122+
}
123+
}
124+
setTimeout(() => exit(), 100);
125+
}
126+
74127
useEffect(() => {
75128
if (status === 'done') {
76-
const hasSecrets = secrets.some(file =>
77-
file.secrets.length > 0
78-
);
129+
const hasSecrets = secrets.some(file => file.secrets.length > 0);
79130
if (hasSecrets) {
80-
setTimeout(() => { process.exitCode = 1; exit(new Error('Secrets detected')); }, 100);
131+
if (hook && process.stdin.isTTY) {
132+
setStatus('prompt_bypass');
133+
} else {
134+
reportBlockAndExit();
135+
}
81136
} else {
82137
setTimeout(() => exit(), 100);
83138
}
@@ -93,6 +148,7 @@ export default function Secrets({ scanType = 'all', include = [], exclude = [],
93148
if (status === 'scanning') return renderScanning(startTime, fileCount, scanMeta);
94149
if (status === 'no_files') return renderNoFiles(scanType, lastNCommits, baseBranch, baseCommit);
95150
if (status === 'error') return renderError(error);
151+
if (status === 'prompt_bypass') return renderBypassPrompt(secrets, handleBypassSelect);
96152
if (status === 'done') return renderDone(secrets, startTime, fileCount, scanMeta);
97153

98154
return null;

src/components/SecretsUI.js

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useState, useEffect } from 'react';
2-
import { Box, Text } from 'ink';
2+
import { Box, Text, useInput } from 'ink';
33

44
// ── Constants ────────────────────────────────────────────────────────────────
55

@@ -326,3 +326,89 @@ export function renderDone(secrets, startTime, fileCount, meta) {
326326
Box, { flexDirection: 'column', paddingX: 1, paddingY: 1 }, ...els,
327327
);
328328
}
329+
330+
331+
// ── Bypass Prompt ───────────────────────────────────────────────────────────
332+
333+
function BypassPrompt({ secrets, onSelect }) {
334+
const [selected, setSelected] = React.useState(0);
335+
const [mode, setMode] = React.useState('menu'); // 'menu' | 'typing'
336+
const [customReason, setCustomReason] = React.useState('');
337+
const allSecrets = secrets.flatMap(file =>
338+
file.secrets.map(s => ({ ...s, file_path: file.file_path }))
339+
);
340+
341+
const options = [
342+
{ label: "It's a false positive", value: 'false_positive' },
343+
{ label: "It's used in tests", value: 'used_in_tests' },
344+
{ label: "I'll fix it later", value: 'fix_later' },
345+
{ label: 'Other (type your reason)', value: 'other' },
346+
{ label: 'Cancel \u2014 block this push', value: 'cancel' },
347+
];
348+
349+
useInput((input, key) => {
350+
if (mode === 'menu') {
351+
if (key.upArrow) {
352+
setSelected(s => (s - 1 + options.length) % options.length);
353+
} else if (key.downArrow) {
354+
setSelected(s => (s + 1) % options.length);
355+
} else if (key.return) {
356+
if (options[selected].value === 'other') {
357+
setMode('typing');
358+
} else {
359+
onSelect(options[selected].value);
360+
}
361+
}
362+
} else {
363+
// typing mode
364+
if (key.return) {
365+
const reason = customReason.trim();
366+
if (reason) {
367+
onSelect('other:' + reason);
368+
}
369+
} else if (key.escape) {
370+
setMode('menu');
371+
setCustomReason('');
372+
} else if (key.backspace || key.delete) {
373+
setCustomReason(s => s.slice(0, -1));
374+
} else if (input && !key.ctrl && !key.meta) {
375+
setCustomReason(s => s + input);
376+
}
377+
}
378+
});
379+
380+
const els = [];
381+
els.push(React.createElement(Text, { key: 'div-top', color: 'gray', dimColor: true }, DIVIDER));
382+
els.push(React.createElement(
383+
Text, { key: 'title', color: 'yellow', bold: true },
384+
`${allSecrets.length} secret(s) detected. Select an action:`,
385+
));
386+
els.push(React.createElement(Text, { key: 'sp' }, ''));
387+
388+
if (mode === 'menu') {
389+
options.forEach((opt, i) => {
390+
const prefix = i === selected ? '> ' : ' ';
391+
const color = i === selected ? 'cyan' : 'white';
392+
els.push(React.createElement(
393+
Text, { key: `opt-${i}`, color, bold: i === selected },
394+
`${prefix}${opt.label}`,
395+
));
396+
});
397+
} else {
398+
els.push(React.createElement(Text, { key: 'typing-label', color: 'cyan' }, ' Type your reason (Enter to submit, Esc to go back):'));
399+
els.push(React.createElement(Text, { key: 'typing-sp' }, ''));
400+
els.push(React.createElement(
401+
Text, { key: 'typing-input', color: 'white' },
402+
` > ${customReason}\u2588`,
403+
));
404+
}
405+
406+
els.push(React.createElement(Text, { key: 'sp2' }, ''));
407+
els.push(React.createElement(Text, { key: 'div-bot', color: 'gray', dimColor: true }, DIVIDER));
408+
409+
return React.createElement(Box, { flexDirection: 'column', paddingX: 1, paddingY: 1 }, ...els);
410+
}
411+
412+
export function renderBypassPrompt(secrets, onSelect) {
413+
return React.createElement(BypassPrompt, { secrets, onSelect });
414+
}

src/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ program
6161
.option('--base-commit <commit>', 'Compare against a specific commit (e.g. --base-commit HEAD~3)')
6262
.option('--include <paths>', 'Comma-separated list of file paths glob patterns to include')
6363
.option('--exclude <paths>', 'Comma-separated list of file paths glob patterns to exclude')
64+
.option('--hook', 'Running from pre-push hook (enables bypass prompt)')
6465
.action((options) => {
6566
let scanType = 'all';
6667
let lastNCommits = 1;
@@ -95,7 +96,7 @@ program
9596
? (Array.isArray(options.exclude) ? options.exclude : splitGlobs(options.exclude))
9697
: [];
9798

98-
render(React.createElement(Secrets, { scanType, include, exclude, lastNCommits, baseBranch, baseCommit }));
99+
render(React.createElement(Secrets, { scanType, include, exclude, lastNCommits, baseBranch, baseCommit, hook: options.hook }));
99100
});
100101

101102
program

src/scm/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,10 @@ export function listCodeReviews(opts) { return getProvider(opts.remote).listCode
9595
export function getCodeReview(opts) { return getProvider(opts.remote).getCodeReview(opts); }
9696
export function searchComments(opts) { return getProvider(opts.remote).searchComments(opts); }
9797
export function resolveConversation(opts) { return getProvider(opts.remote).resolveConversation(opts); }
98+
99+
// Auto-detect base URL (git host) from origin
100+
export function detectBaseUrl() {
101+
const origin = getOrigin();
102+
if (!origin) return '';
103+
const host = parseOriginUrl(origin); return host ? 'https://' + host : '';
104+
}

src/utils/installPushProtectionHook.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ function buildHookBlock() {
2222
# Auto-installed by CodeAnt AI — blocks pushes containing secrets.
2323
# To disable: delete this hook or run "codeant push-protection disable"
2424
command -v codeant >/dev/null 2>&1 || exit 0
25-
codeant secrets --committed
25+
codeant secrets --committed --hook
2626
${HOOK_MARKER_END}`;
2727
}
2828

0 commit comments

Comments
 (0)