From db3d6f1b3b2a048c0e985949a0a82e15f0a9a0b8 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 1 Jun 2026 00:56:27 +0900 Subject: [PATCH 1/7] Fix excerpt line aliases (#1429) --- changelog.d/unreleased/1429.fixed.md | 17 +++++++++++++++++ src/CodeIndex/Cli/CliFlagSchema.cs | 2 ++ src/CodeIndex/Cli/QueryCommandRunner.cs | 12 ++++++++---- .../CodeIndex.Tests/QueryCommandRunnerTests.cs | 12 ++++++++++++ 4 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 changelog.d/unreleased/1429.fixed.md diff --git a/changelog.d/unreleased/1429.fixed.md b/changelog.d/unreleased/1429.fixed.md new file mode 100644 index 0000000000..5f47f09d54 --- /dev/null +++ b/changelog.d/unreleased/1429.fixed.md @@ -0,0 +1,17 @@ +--- +category: fixed +issues: + - 1429 +affected: + - src/CodeIndex/Cli/QueryCommandRunner.cs + - src/CodeIndex/Cli/CliFlagSchema.cs + - tests/CodeIndex.Tests/QueryCommandRunnerTests.cs +--- + +## English + +- **`excerpt` accepts MCP-style line aliases (#1429)** - `cdidx excerpt` now accepts `--start-line` and `--end-line` as aliases for `--start` and `--end`, reducing friction when moving between MCP and CLI usage. + +## 日本語 + +- **`excerpt` が MCP 形式の行番号 alias を受け付けるようになりました (#1429)** - `cdidx excerpt` は `--start` / `--end` の alias として `--start-line` / `--end-line` を受け付けるようになり、MCP と CLI の間を行き来するときのつまずきを減らします。 diff --git a/src/CodeIndex/Cli/CliFlagSchema.cs b/src/CodeIndex/Cli/CliFlagSchema.cs index 25a301eb49..995932997a 100644 --- a/src/CodeIndex/Cli/CliFlagSchema.cs +++ b/src/CodeIndex/Cli/CliFlagSchema.cs @@ -256,7 +256,9 @@ private static IReadOnlyList BuildAll() new() { Name = "--before", ValuePlaceholder = "", Description = "Context lines before", Commands = Set("find", "excerpt") }, new() { Name = "--after", ValuePlaceholder = "", Description = "Context lines after", Commands = Set("find", "excerpt") }, new() { Name = "--start", ValuePlaceholder = "", Description = "Start line", Commands = Set("excerpt") }, + new() { Name = "--start-line", ValuePlaceholder = "", Description = "Alias for --start", Commands = Set("excerpt") }, new() { Name = "--end", ValuePlaceholder = "", Description = "End line", Commands = Set("excerpt") }, + new() { Name = "--end-line", ValuePlaceholder = "", Description = "Alias for --end", Commands = Set("excerpt") }, new() { Name = "--focus-line", ValuePlaceholder = "", Description = "Focused line to keep visible when clamping", Commands = Set("find", "excerpt") }, new() { Name = "--focus-column", ValuePlaceholder = "", Description = "Focused column to keep visible when clamping", Commands = Set("find", "excerpt") }, new() { Name = "--focus-length", ValuePlaceholder = "", Description = "Focused span width when clamping", Commands = Set("excerpt") }, diff --git a/src/CodeIndex/Cli/QueryCommandRunner.cs b/src/CodeIndex/Cli/QueryCommandRunner.cs index 413ee74187..87e3e5219c 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.cs @@ -5417,9 +5417,11 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue) AddParseError($"Error: could not parse --since value '{sinceValue}' as a date/time. Use ISO 8601 format (e.g. 2024-01-01 or 2024-01-01T00:00:00Z)."); break; case "--start": - if (!TryReadRawOptionValue(args, ref i, "--start", inlineValue, out var startValue, out var missingStartError)) + case "--start-line": + var startFlag = normalizedArg; + if (!TryReadRawOptionValue(args, ref i, startFlag, inlineValue, out var startValue, out var missingStartError)) AddParseError(missingStartError!); - else if (TryParsePositiveInt(startValue!, "--start", out var parsedStart, out var startError)) + else if (TryParsePositiveInt(startValue!, startFlag, out var parsedStart, out var startError)) { WarnIfDuplicateSingleValueOption("--start", startValue!); startLine = parsedStart; @@ -5428,9 +5430,11 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue) AddParseError(startError!); break; case "--end": - if (!TryReadRawOptionValue(args, ref i, "--end", inlineValue, out var endValue, out var missingEndError)) + case "--end-line": + var endFlag = normalizedArg; + if (!TryReadRawOptionValue(args, ref i, endFlag, inlineValue, out var endValue, out var missingEndError)) AddParseError(missingEndError!); - else if (TryParsePositiveInt(endValue!, "--end", out var parsedEnd, out var endError)) + else if (TryParsePositiveInt(endValue!, endFlag, out var parsedEnd, out var endError)) { WarnIfDuplicateSingleValueOption("--end", endValue!); endLine = parsedEnd; diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs index 2df6887174..d824f7758e 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs @@ -29514,6 +29514,18 @@ public void RunExcerpt_RejectsStartGreaterThanEnd() Assert.Contains("--start (5) must be less than or equal to --end (3)", stderr); } + [Fact] + public void RunExcerpt_AcceptsMcpStyleStartAndEndLineAliases() + { + var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunExcerpt( + ["src/app.cs", "--start-line", "5", "--end-line", "3"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Contains("--start (5) must be less than or equal to --end (3)", stderr); + Assert.DoesNotContain("unsupported option", stderr, StringComparison.OrdinalIgnoreCase); + } + [Fact] public void RunInspect_BlankQueryReturnsDistinctUsageError() { From 91a65518c9c0723377c1311df98361f75609413e Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 1 Jun 2026 00:56:43 +0900 Subject: [PATCH 2/7] Fix bash guard argument false positives (#1432) --- .agent_harness/command_guard_core.py | 15 ++++++--------- .agent_harness/tests/test_command_guard_core.py | 12 ++++++++++++ changelog.d/unreleased/1432.fixed.md | 16 ++++++++++++++++ 3 files changed, 34 insertions(+), 9 deletions(-) create mode 100644 changelog.d/unreleased/1432.fixed.md diff --git a/.agent_harness/command_guard_core.py b/.agent_harness/command_guard_core.py index 67c6e28772..3f0b554b47 100644 --- a/.agent_harness/command_guard_core.py +++ b/.agent_harness/command_guard_core.py @@ -135,10 +135,6 @@ VARIABLE_COMMAND_RE = re.compile(r"^\$(?:[A-Za-z_][A-Za-z0-9_]*|\{[A-Za-z_][A-Za-z0-9_]*\})$") _FORBIDDEN_COMMAND_PATTERNS: list[tuple[re.Pattern[str], str]] = [ - (SEARCH_OR_DISCOVERY_RE, "shell search/file-discovery command is blocked; use dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll instead."), - (GLOBAL_CDIDX_RE, "global cdidx is blocked; use dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll instead, or the fully expanded installed path documented in CLOUD_BOOTSTRAP_PROMPT.md for no-SDK cloud bootstrap."), - (GIT_GREP_RE, "git grep is blocked; use dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll instead."), - (EVAL_RE, "eval is blocked; use a direct reviewed command or script file instead."), (re.compile(r"(?i)\brm\s+-[^\n;|&]*r[^\n;|&]*f\b|\brm\s+-[^\n;|&]*f[^\n;|&]*r\b"), "recursive forced rm is blocked"), (re.compile(r"(?i)\brm\s+-r\b"), "recursive rm is blocked"), (re.compile(r"(?i)\b(?:rmdir|unlink|shred|srm|truncate)\b"), "destructive filesystem command is blocked"), @@ -152,7 +148,6 @@ (re.compile(r"(?i)\b(?:curl|wget)\b.*\|\s*(?:sh|bash|zsh|python|ruby|perl)\b"), "download-and-execute is blocked"), (re.compile(r"(?i)\b(?:ssh|scp|sftp|rsync|rclone|nc|ncat|netcat|socat|telnet|ftp)\b"), "remote shell/file transfer is blocked"), (re.compile(r"(?i)\b(?:pbcopy|pbpaste)\b"), "clipboard access is blocked"), - (re.compile(r"(?i)\b(?:open|osascript|automator)\b|\bshortcuts\s+run\b"), "macOS automation/app launching is blocked"), (re.compile(r"(?i)\b(?:launchctl|security|tccutil|spctl|csrutil|tmutil)\b"), "macOS security/system command is blocked"), (re.compile(r"(?i)\bdefaults\s+write\b|\bplutil\s+-replace\b"), "macOS preference modification is blocked"), (re.compile(r"(?i)\bgit\s+tag\b"), "git tag is blocked unless explicitly performed by the user"), @@ -173,8 +168,6 @@ ] _SCRIPT_FORBIDDEN_PATTERNS: list[tuple[re.Pattern[str], str]] = [ - (SEARCH_OR_DISCOVERY_RE, "script contains shell search/file-discovery command"), - (GIT_GREP_RE, "script contains git grep"), *_FORBIDDEN_COMMAND_PATTERNS, ] @@ -542,11 +535,15 @@ def _tokenized_forbidden_tokens_reason(tokens: list[str]) -> str | None: if _env_chdir_wrapper_present(tokens): return "env chdir wrappers are blocked; run from the target cwd directly" - for index, token in enumerate(tokens): + for segment in _token_segments(tokens): + segment = _strip_transparent_script_wrappers(_strip_leading_env_assignments(segment)) + if not segment: + continue + token = segment[0] if not token or token.startswith("-") or _is_env_assignment(token): continue name = _token_command_name(token) - args = tokens[index + 1 :] + args = segment[1:] if name in _SEARCH_OR_DISCOVERY_COMMANDS: return "shell search/file-discovery command is blocked; use dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll instead." if name in _NETWORK_COMMANDS: diff --git a/.agent_harness/tests/test_command_guard_core.py b/.agent_harness/tests/test_command_guard_core.py index 8873b3b831..aa4be4341a 100644 --- a/.agent_harness/tests/test_command_guard_core.py +++ b/.agent_harness/tests/test_command_guard_core.py @@ -234,6 +234,18 @@ def test_denies_global_cdidx_and_search_tools(self) -> None: decision = core.evaluate_bash_command(command, cwd=root, project_root=root) self.assertFalse(decision.allowed) + def test_allows_forbidden_command_words_inside_gh_arguments(self) -> None: + root = Path("/tmp") + for command in ( + 'gh issue create --title "find and open are issue words" --body "cdidx find appears in docs"', + 'gh issue list --state open --search "grep in the issue body"', + 'gh pr create --title "search: find hint" --body "users may type git grep or open here"', + ): + with self.subTest(command=command): + decision = core.evaluate_bash_command(command, cwd=root, project_root=root) + + self.assertTrue(decision.allowed, decision.reason) + def test_denies_quote_concatenated_high_risk_commands(self) -> None: root = Path("/tmp") for command in ( diff --git a/changelog.d/unreleased/1432.fixed.md b/changelog.d/unreleased/1432.fixed.md new file mode 100644 index 0000000000..5753a13873 --- /dev/null +++ b/changelog.d/unreleased/1432.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 1432 +affected: + - .agent_harness/command_guard_core.py + - .agent_harness/tests/test_command_guard_core.py +--- + +## English + +- **Agent bash guard no longer blocks command-name words inside GitHub arguments (#1432)** - the shared guard now applies command-name checks to executed command tokens instead of quoted title/body text, so `gh issue` and `gh pr` workflows can mention words such as `find`, `open`, `grep`, or `cdidx`. + +## 日本語 + +- **エージェント用 bash guard が GitHub 引数内のコマンド名単語で誤拒否しなくなりました (#1432)** - 共有 guard は quoted title/body ではなく実行されるコマンド token に対してコマンド名チェックを行うため、`gh issue` や `gh pr` の本文で `find`、`open`、`grep`、`cdidx` などの語を扱えます。 From 489a0fee63b3e3aa1244926313a16e94d836ac05 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 1 Jun 2026 00:57:21 +0900 Subject: [PATCH 3/7] Add progress animation opt-out (#1630) --- README.md | 4 +- changelog.d/unreleased/1630.added.md | 19 +++++++++ src/CodeIndex/Cli/CliFlagSchema.cs | 1 + src/CodeIndex/Cli/ConsoleUi.cs | 32 ++++++++++++++- src/CodeIndex/Cli/ProgramRunner.cs | 35 ++++++++++++++++ tests/CodeIndex.Tests/ConsoleUiTests.cs | 54 ++++++++++++++++++++++++- 6 files changed, 141 insertions(+), 4 deletions(-) create mode 100644 changelog.d/unreleased/1630.added.md diff --git a/README.md b/README.md index d284865d9f..85dcd1d262 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ Output controls: | Owner-only persistent stderr logs on POSIX | Global tool stderr logs are forced to `0600` permissions on every open, including existing date-stamped log files. Use `--log-format text|json`, `--log-retain-count `, `--log-max-size-mb `, or the matching `CDIDX_LOG_*` environment variables to make lifecycle logs JSONL-friendly and rotate them for aggregation. | | Checked-in configuration | Use `.cdidx/config.json` for repository defaults such as `search.limit`, `search.snippet_lines`, and `search.max_line_width`; run `cdidx validate-config` to validate the discovered file and `cdidx config show` to inspect precedence. | | Workspaces | Use `cdidx.workspace.json` or `.cdidx-workspace.json` to declare monorepo members, `cdidx workspace list` to inspect them, and `cdidx workspace use ` / `cdidx workspace current` for a persisted active workspace. | -| ASCII-only terminal output | Use `--ascii`, `CDIDX_ASCII=1`, `NO_UNICODE`, `TERM=dumb`, accessibility env hints, or a non-UTF-8 locale. Spinners use pipe, slash, dash, and backslash frames; progress bars use `#` / `-`; very narrow terminals fall back to percentage-only progress. | +| ASCII-only terminal output | Use `--ascii`, `CDIDX_ASCII=1`, `NO_UNICODE`, `TERM=dumb`, accessibility env hints, or a non-UTF-8 locale. Spinners use pipe, slash, dash, and backslash frames; progress bars use `#` / `-`; very narrow terminals fall back to percentage-only progress. Use `--no-progress`, `CDIDX_DISABLE_PROGRESS=1`, or `PREFERS_REDUCED_MOTION` to keep static progress text without animation. | | Color and terminal capability | `--color auto` emits ANSI only for capable interactive terminals; `TERM=dumb`, `CI=true`, missing Unix terminal hints, `NO_COLOR`, or `CLICOLOR=0` disable ANSI/progress control sequences. `--palette basic|256|truecolor` can override the `COLORTERM` / `TERM` color-depth detection. | | UTF-8 JSON pipelines | CLI `--json` output is written as UTF-8 without a BOM and never includes ANSI escape sequences, even when color is forced for human output. | | Script-friendly query pipelines | Use `--quiet`, `-q`, `--silent`, or `CDIDX_QUIET=1` to suppress informational stderr text while preserving errors. `--quiet` takes precedence over `--verbose`. Read commands that support `--format` can emit `count`, `compact`, `csv`, or `tsv` output when callers need smaller or table-shaped payloads instead of full excerpts. | @@ -345,7 +345,7 @@ extractor fixture を確認できます。詳細は | POSIX の persistent stderr log を owner-only にする | global tool stderr log は開くたびに `0600` 権限へ補正され、既存の日付付き log file も同じ扱いになります。 | | checked-in configuration | repository 既定値には `.cdidx/config.json` を使えます。例: `search.limit`、`search.snippet_lines`、`search.max_line_width`。検出された file は `cdidx validate-config` で検証でき、`cdidx config show` で優先順位を確認できます。 | | workspaces | monorepo member は `cdidx.workspace.json` または `.cdidx-workspace.json` で宣言し、`cdidx workspace list` で確認できます。`cdidx workspace use ` / `cdidx workspace current` は永続 active workspace を扱います。 | -| ASCII-only 端末で崩れない表示にする | `--ascii`、`CDIDX_ASCII=1`、`NO_UNICODE`、`TERM=dumb`、accessibility 系の環境変数、非 UTF-8 locale を使います。スピナーは pipe、slash、dash、backslash の frame、進捗バーは `#` / `-` になり、幅が非常に狭い端末では percentage-only になります。 | +| ASCII-only 端末で崩れない表示にする | `--ascii`、`CDIDX_ASCII=1`、`NO_UNICODE`、`TERM=dumb`、accessibility 系の環境変数、非 UTF-8 locale を使います。スピナーは pipe、slash、dash、backslash の frame、進捗バーは `#` / `-` になり、幅が非常に狭い端末では percentage-only になります。`--no-progress`、`CDIDX_DISABLE_PROGRESS=1`、`PREFERS_REDUCED_MOTION` を使うと animation なしの静的な進捗表示になります。 | | color と端末 capability | `--color auto` は対応する interactive terminal でだけ ANSI を出力します。`TERM=dumb`、`CI=true`、Unix で端末 hint が無い場合、`NO_COLOR`、`CLICOLOR=0` では ANSI / progress 制御シーケンスを抑止します。`--palette basic|256|truecolor` で `COLORTERM` / `TERM` による color-depth 判定を上書きできます。 | | UTF-8 JSON pipeline | CLI の `--json` 出力は BOM なし UTF-8 で書き出され、human output 向けに色を強制していても ANSI escape sequence を含みません。 | | script 向け query pipeline の stderr を静かにする | `--quiet`、`-q`、`--silent`、`CDIDX_QUIET=1` で informational stderr を抑制し、error 行だけを残します。`--quiet` は `--verbose` より優先されます。 | diff --git a/changelog.d/unreleased/1630.added.md b/changelog.d/unreleased/1630.added.md new file mode 100644 index 0000000000..75139d2e47 --- /dev/null +++ b/changelog.d/unreleased/1630.added.md @@ -0,0 +1,19 @@ +--- +category: added +issues: + - 1630 +affected: + - src/CodeIndex/Cli/ConsoleUi.cs + - src/CodeIndex/Cli/ProgramRunner.cs + - src/CodeIndex/Cli/CliFlagSchema.cs + - README.md + - tests/CodeIndex.Tests/ConsoleUiTests.cs +--- + +## English + +- **Progress animation can now be disabled (#1630)** - `cdidx` honors `--no-progress`, `CDIDX_DISABLE_PROGRESS=1`, and `PREFERS_REDUCED_MOTION` by printing static progress text instead of animated spinner frames. + +## 日本語 + +- **進捗 animation を無効化できるようになりました (#1630)** - `cdidx` は `--no-progress`、`CDIDX_DISABLE_PROGRESS=1`、`PREFERS_REDUCED_MOTION` を尊重し、animated spinner frame の代わりに静的な進捗テキストを表示します。 diff --git a/src/CodeIndex/Cli/CliFlagSchema.cs b/src/CodeIndex/Cli/CliFlagSchema.cs index 995932997a..1fb4c1b970 100644 --- a/src/CodeIndex/Cli/CliFlagSchema.cs +++ b/src/CodeIndex/Cli/CliFlagSchema.cs @@ -246,6 +246,7 @@ private static IReadOnlyList BuildAll() new() { Name = "--exact-name", Description = "Exact symbol-name equality", Commands = Set(ExactNameCommands), AlsoAcceptedBy = Set("search") }, new() { Name = "--exact-substring", Description = "Search-only exact substring match", Commands = Set("search"), AlsoAcceptedBy = Set(ExactSubstringAccepted) }, new() { Name = "--prefix", Description = "Trailing-asterisk prefix shorthand", Commands = Set("search") }, + new() { Name = "--no-progress", Description = "Disable animated progress and spinner output", Commands = Set(AllCommands.ToArray()) }, new() { Name = "--name", ValuePlaceholder = "", Description = "Exact symbol name", Commands = Set("symbols") }, new() { Name = "--max-line-width", ValuePlaceholder = "", Description = "Clamp long single-line payloads (0 disables clamping)", Commands = Set(MaxLineWidthCommands) }, new() { Name = "--snippet-lines", ValuePlaceholder = "", Description = "Snippet length", Commands = Set("search", "find", "references", "callers", "callees", "impact") }, diff --git a/src/CodeIndex/Cli/ConsoleUi.cs b/src/CodeIndex/Cli/ConsoleUi.cs index 9ddb85212a..4557f14e7a 100644 --- a/src/CodeIndex/Cli/ConsoleUi.cs +++ b/src/CodeIndex/Cli/ConsoleUi.cs @@ -64,6 +64,8 @@ public enum CompletionNotificationMode /// public static class ConsoleUi { + public const string DisableProgressEnvironmentVariable = "CDIDX_DISABLE_PROGRESS"; + public const string PrefersReducedMotionEnvironmentVariable = "PREFERS_REDUCED_MOTION"; public const int SummaryLabelWidth = 9; private static readonly (string Command, string Usage)[] CommandUsageLines = @@ -262,7 +264,7 @@ private static string FormatDurationAsHms(TimeSpan duration) // ブレイルフレームは1文字、テーマフレームは表示テキストを含む長い文字列 bool isThemed = frames.Length > 0 && frames[0].Length > 2; - if (!ShouldUseInteractiveConsole()) + if (!ShouldUseInteractiveConsole() || !ShouldUseProgressAnimation()) { Console.WriteLine(message); return null; @@ -310,6 +312,21 @@ public static void StopSpinner(CancellationTokenSource? cts) cts.Dispose(); } + internal static void SetProgressAnimationEnabled(bool? enabled) + => _progressAnimationEnabledOverride = enabled; + + internal static bool ShouldUseProgressAnimation() + { + if (_progressAnimationEnabledOverride.HasValue) + return _progressAnimationEnabledOverride.Value; + + if (IsTruthyEnvironmentVariable(DisableProgressEnvironmentVariable)) + return false; + + var reducedMotion = Environment.GetEnvironmentVariable(PrefersReducedMotionEnvironmentVariable); + return string.IsNullOrWhiteSpace(reducedMotion) || !IsTruthyEnvironmentValue(reducedMotion); + } + /// /// Get spinner frames based on easter egg flag. /// イースターエッグフラグに基づくスピナーフレームを取得。 @@ -382,6 +399,7 @@ public static string[] GetSpinnerFrames(string? easterEgg) // Track last progress line length for clearing / クリア用に最後のプログレス行の長さを記録 private static int _lastProgressLineLength; private static bool _asciiOutputForced; + private static bool? _progressAnimationEnabledOverride; private static bool _widthDetectionFailed; private static bool _widthDetectionTraceWritten; private static bool _traceWidthDetectionFailures; @@ -886,6 +904,7 @@ private static void PrintFlagReference(Action WriteHelpLine) WriteHelpLine(" --color Color output: `auto` (default), `always`, or `never`; flag wins over `CLICOLOR_FORCE` / `NO_COLOR` / `CLICOLOR` env vars, which win over TTY auto-detect"); WriteHelpLine(" --palette ANSI palette: `basic` (8-color, default fallback), `256`, or `truecolor`; flag wins over `CDIDX_COLOR_PALETTE` env var, which wins over `COLORTERM` / `TERM` auto-detect"); WriteHelpLine(" --ascii Use ASCII spinner/progress glyphs instead of Unicode glyphs (also honors CDIDX_ASCII=1, NO_UNICODE, TERM=dumb, accessibility env hints, and non-UTF-8 locales)"); + WriteHelpLine(" --no-progress Disable animated progress/spinner output (also honors CDIDX_DISABLE_PROGRESS=1 and PREFERS_REDUCED_MOTION)"); Console.WriteLine(" --metrics Append one JSONL record per CLI command / MCP tool call to (also honors CDIDX_METRICS=)"); Console.WriteLine(" --help, -h Show this help message"); Console.WriteLine(" --version, -V Show version information"); @@ -2071,6 +2090,17 @@ private static bool IsAsciiOutputRequested() || IsPosixLocale(Environment.GetEnvironmentVariable("LANG")); } + private static bool IsTruthyEnvironmentVariable(string name) + => IsTruthyEnvironmentValue(Environment.GetEnvironmentVariable(name)); + + private static bool IsTruthyEnvironmentValue(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return false; + + return value.Trim() is not ("0" or "false" or "False" or "FALSE" or "no" or "No" or "NO"); + } + private static bool IsDumbTerminal() => string.Equals(Environment.GetEnvironmentVariable("TERM"), "dumb", StringComparison.OrdinalIgnoreCase); diff --git a/src/CodeIndex/Cli/ProgramRunner.cs b/src/CodeIndex/Cli/ProgramRunner.cs index d046db6307..c9c4b4fc60 100644 --- a/src/CodeIndex/Cli/ProgramRunner.cs +++ b/src/CodeIndex/Cli/ProgramRunner.cs @@ -70,6 +70,7 @@ internal static int Run( } TryConsumeAsciiFlag(ref args); + TryConsumeNoProgressFlag(ref args); if (!TryConsumeMetricsFlag(ref args, out var metricsPath, out var metricsError)) { @@ -869,6 +870,40 @@ internal static void TryConsumeAsciiFlag(ref string[] args) args = kept.ToArray(); } + internal static void TryConsumeNoProgressFlag(ref string[] args) + { + ConsoleUi.SetProgressAnimationEnabled(null); + if (args.Length == 0) + return; + + var kept = new List(args.Length); + var passthrough = false; + for (var i = 0; i < args.Length; i++) + { + var arg = args[i]; + if (passthrough) + { + kept.Add(arg); + continue; + } + if (arg == "--") + { + passthrough = true; + kept.Add(arg); + continue; + } + if (arg == "--no-progress") + { + ConsoleUi.SetProgressAnimationEnabled(false); + continue; + } + + kept.Add(arg); + } + + args = kept.ToArray(); + } + // Strip `--palette ` / `--palette=` from `args` before // subcommand parsing. Mirrors `TryConsumeColorFlag` so any subcommand // (CLI or MCP) inherits the chosen ANSI palette without re-parsing. diff --git a/tests/CodeIndex.Tests/ConsoleUiTests.cs b/tests/CodeIndex.Tests/ConsoleUiTests.cs index 38daccbd27..f6e88baf1e 100644 --- a/tests/CodeIndex.Tests/ConsoleUiTests.cs +++ b/tests/CodeIndex.Tests/ConsoleUiTests.cs @@ -436,6 +436,36 @@ public void StartSpinner_RedirectedOutput_UsesSynchronizedWriterBeforeFallbackLi Assert.Contains("Indexing...", output.ToString()); } + [Fact] + public void ShouldUseProgressAnimation_EnvOptOutDisablesAnimation() + { + using var env = new ProgressAnimationEnvironmentScope(); + Environment.SetEnvironmentVariable(ConsoleUi.DisableProgressEnvironmentVariable, "1"); + + Assert.False(ConsoleUi.ShouldUseProgressAnimation()); + } + + [Fact] + public void ShouldUseProgressAnimation_ReducedMotionDisablesAnimation() + { + using var env = new ProgressAnimationEnvironmentScope(); + Environment.SetEnvironmentVariable(ConsoleUi.PrefersReducedMotionEnvironmentVariable, "1"); + + Assert.False(ConsoleUi.ShouldUseProgressAnimation()); + } + + [Fact] + public void TryConsumeNoProgressFlag_RemovesGlobalFlagAndDisablesAnimation() + { + using var env = new ProgressAnimationEnvironmentScope(); + string[] args = ["index", ".", "--no-progress", "--json"]; + + ProgramRunner.TryConsumeNoProgressFlag(ref args); + + Assert.Equal(["index", ".", "--json"], args); + Assert.False(ConsoleUi.ShouldUseProgressAnimation()); + } + [Fact] public void GetWindowWidth_ColumnsEnvVarSet_UsesColumnsValue() { @@ -839,7 +869,7 @@ public void PrintCompletions_ReportFlagSetsMatchAcrossShells() var expected = new SortedSet(StringComparer.Ordinal) { - "db", "json", "quiet", "silent", "output", "log-lines", "no-log", "include-args", + "db", "json", "quiet", "silent", "no-progress", "output", "log-lines", "no-log", "include-args", }; Assert.Equal(expected, bashReport); Assert.Equal(expected, zshReport); @@ -1700,6 +1730,28 @@ public void Dispose() } } + private sealed class ProgressAnimationEnvironmentScope : IDisposable + { + private readonly string? _originalDisableProgress; + private readonly string? _originalReducedMotion; + + public ProgressAnimationEnvironmentScope() + { + _originalDisableProgress = Environment.GetEnvironmentVariable(ConsoleUi.DisableProgressEnvironmentVariable); + _originalReducedMotion = Environment.GetEnvironmentVariable(ConsoleUi.PrefersReducedMotionEnvironmentVariable); + Environment.SetEnvironmentVariable(ConsoleUi.DisableProgressEnvironmentVariable, null); + Environment.SetEnvironmentVariable(ConsoleUi.PrefersReducedMotionEnvironmentVariable, null); + ConsoleUi.SetProgressAnimationEnabled(null); + } + + public void Dispose() + { + Environment.SetEnvironmentVariable(ConsoleUi.DisableProgressEnvironmentVariable, _originalDisableProgress); + Environment.SetEnvironmentVariable(ConsoleUi.PrefersReducedMotionEnvironmentVariable, _originalReducedMotion); + ConsoleUi.SetProgressAnimationEnabled(null); + } + } + private sealed class ColorEnvironmentScope : IDisposable { private readonly bool _lockTaken; From 279dee1668e932765d18729d7fb8c9a61655b719 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 1 Jun 2026 01:23:09 +0900 Subject: [PATCH 4/7] Fix cdidx guard argument false positives (#1432) --- .agent_harness/command_guard_core.py | 6 +++++- .agent_harness/tests/test_command_guard_core.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.agent_harness/command_guard_core.py b/.agent_harness/command_guard_core.py index 3f0b554b47..e8e5516373 100644 --- a/.agent_harness/command_guard_core.py +++ b/.agent_harness/command_guard_core.py @@ -300,7 +300,11 @@ def _token_is_forbidden_cdidx_executable(token: str, cwd: Path) -> bool: def _command_mentions_forbidden_cdidx_executable(command: str, cwd: Path) -> bool: tokens = _split_command(command) - return any(_token_is_forbidden_cdidx_executable(token, cwd) for token in tokens) + for segment in _token_segments(tokens): + segment = _strip_transparent_script_wrappers(_strip_leading_env_assignments(segment)) + if segment and _token_is_forbidden_cdidx_executable(segment[0], cwd): + return True + return False def _command_is_safe_expanded_installed_cdidx(command: str, cwd: Path) -> bool: diff --git a/.agent_harness/tests/test_command_guard_core.py b/.agent_harness/tests/test_command_guard_core.py index aa4be4341a..cc49a1c420 100644 --- a/.agent_harness/tests/test_command_guard_core.py +++ b/.agent_harness/tests/test_command_guard_core.py @@ -237,6 +237,7 @@ def test_denies_global_cdidx_and_search_tools(self) -> None: def test_allows_forbidden_command_words_inside_gh_arguments(self) -> None: root = Path("/tmp") for command in ( + 'gh issue create --title "cdidx" --body "plain issue text"', 'gh issue create --title "find and open are issue words" --body "cdidx find appears in docs"', 'gh issue list --state open --search "grep in the issue body"', 'gh pr create --title "search: find hint" --body "users may type git grep or open here"', From ea25542504b355893f625ba4791d743e024c0558 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 1 Jun 2026 01:28:26 +0900 Subject: [PATCH 5/7] Disable progress bar animation opt-out (#1630) --- src/CodeIndex/Cli/ConsoleUi.cs | 12 +++++++++--- tests/CodeIndex.Tests/ConsoleUiTests.cs | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/CodeIndex/Cli/ConsoleUi.cs b/src/CodeIndex/Cli/ConsoleUi.cs index 5d9e7ea37d..f70eec4c1b 100644 --- a/src/CodeIndex/Cli/ConsoleUi.cs +++ b/src/CodeIndex/Cli/ConsoleUi.cs @@ -434,7 +434,8 @@ public static void PrintProgress(int current, int total) current, total, redirected ? 80 : GetWindowWidth(), - ShouldUseUnicodeGlyphs()); + ShouldUseUnicodeGlyphs(), + ShouldUseProgressAnimation()); if (!redirected) { @@ -457,7 +458,12 @@ public static void PrintProgress(int current, int total) } } - internal static string FormatProgressLine(int current, int total, int windowWidth, bool useUnicodeGlyphs) + internal static string FormatProgressLine( + int current, + int total, + int windowWidth, + bool useUnicodeGlyphs, + bool useProgressAnimation = true) { const int barWidth = 32; var pct = (double)current / total; @@ -472,7 +478,7 @@ internal static string FormatProgressLine(int current, int total, int windowWidt if (filled > barWidth) filled = barWidth; if (filled < 0) filled = 0; - var spinner = ResolveProgressSpinner(current, total, useUnicodeGlyphs); + var spinner = useProgressAnimation ? ResolveProgressSpinner(current, total, useUnicodeGlyphs) : " "; var bar = useUnicodeGlyphs ? new string('\u2588', filled) + new string('\u2591', barWidth - filled) : $"[{new string('#', filled)}{new string('-', barWidth - filled)}]"; diff --git a/tests/CodeIndex.Tests/ConsoleUiTests.cs b/tests/CodeIndex.Tests/ConsoleUiTests.cs index f6e88baf1e..b5aff23ab9 100644 --- a/tests/CodeIndex.Tests/ConsoleUiTests.cs +++ b/tests/CodeIndex.Tests/ConsoleUiTests.cs @@ -370,6 +370,20 @@ public void FormatProgressLine_AsciiFallback_UsesAsciiBarAndSpinner() Assert.DoesNotContain('░', line); } + [Fact] + public void FormatProgressLine_ProgressAnimationDisabled_UsesStaticPrefix() + { + var line = ConsoleUi.FormatProgressLine( + 25, + 100, + windowWidth: 80, + useUnicodeGlyphs: false, + useProgressAnimation: false); + + Assert.StartsWith(" [########", line); + Assert.DoesNotContain("- [", line); + } + [Fact] public void FormatProgressLine_NarrowUnicodeTerminal_UsesPercentageOnly() { From 1b51eb1da0a9e154bbe6a693725bc577dff7420c Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 1 Jun 2026 01:32:47 +0900 Subject: [PATCH 6/7] Preserve script guard search checks (#1432) --- .agent_harness/command_guard_core.py | 2 ++ .agent_harness/tests/test_command_guard_core.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/.agent_harness/command_guard_core.py b/.agent_harness/command_guard_core.py index e8e5516373..ff0f8e3265 100644 --- a/.agent_harness/command_guard_core.py +++ b/.agent_harness/command_guard_core.py @@ -168,6 +168,8 @@ ] _SCRIPT_FORBIDDEN_PATTERNS: list[tuple[re.Pattern[str], str]] = [ + (SEARCH_OR_DISCOVERY_RE, "script contains shell search/file-discovery command"), + (GIT_GREP_RE, "script contains git grep"), *_FORBIDDEN_COMMAND_PATTERNS, ] diff --git a/.agent_harness/tests/test_command_guard_core.py b/.agent_harness/tests/test_command_guard_core.py index cc49a1c420..5d429f19fe 100644 --- a/.agent_harness/tests/test_command_guard_core.py +++ b/.agent_harness/tests/test_command_guard_core.py @@ -384,6 +384,22 @@ def test_check_script_file_denies_forbidden_content(self) -> None: self.assertFalse(decision.allowed) + def test_check_script_file_denies_search_commands_inside_shell_constructs(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + script = root / "tools" / "guard.sh" + script.parent.mkdir(parents=True, exist_ok=True) + + for body in ( + "if grep SymbolExtractor src; then echo yes; fi\n", + "if git grep SymbolExtractor; then echo yes; fi\n", + ): + with self.subTest(body=body): + script.write_text(body, encoding="utf-8") + decision = core.check_script_file(script, project_root=root) + + self.assertFalse(decision.allowed) + def test_check_script_file_denies_quote_concatenated_forbidden_content(self) -> None: with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) From cc0e1a77e7b9bb5330b526e481c15b963a02fefc Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 1 Jun 2026 01:35:54 +0900 Subject: [PATCH 7/7] Handle shell keyword guard commands (#1432) --- .agent_harness/command_guard_core.py | 3 +++ .agent_harness/tests/test_command_guard_core.py | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/.agent_harness/command_guard_core.py b/.agent_harness/command_guard_core.py index ff0f8e3265..393ca31487 100644 --- a/.agent_harness/command_guard_core.py +++ b/.agent_harness/command_guard_core.py @@ -65,6 +65,7 @@ _CLOUD_COMMANDS = {"aws", "gcloud", "az"} _SECRET_FILE_READ_COMMANDS = {"cat", "less", "more", "head", "tail", "sed", "awk", "python", "python3", "node", "ruby", "perl", "sqlite3"} _CWD_CHANGING_COMMANDS = {"cd", "pushd", "popd"} +_SHELL_COMMAND_PREFIX_KEYWORDS = {"if", "then", "elif", "else", "while", "until", "do"} _SECRET_PATH_RE = re.compile(r"(?i)(?:\.env\b|\.env\.|\.pem\b|\.key\b|id_rsa|id_ed25519|credentials?|secrets?)") ANSI_C_QUOTE_RE = re.compile(r"\$'") @@ -543,6 +544,8 @@ def _tokenized_forbidden_tokens_reason(tokens: list[str]) -> str | None: for segment in _token_segments(tokens): segment = _strip_transparent_script_wrappers(_strip_leading_env_assignments(segment)) + while segment and segment[0] in _SHELL_COMMAND_PREFIX_KEYWORDS: + segment = _strip_transparent_script_wrappers(_strip_leading_env_assignments(segment[1:])) if not segment: continue token = segment[0] diff --git a/.agent_harness/tests/test_command_guard_core.py b/.agent_harness/tests/test_command_guard_core.py index 5d429f19fe..b80c63128c 100644 --- a/.agent_harness/tests/test_command_guard_core.py +++ b/.agent_harness/tests/test_command_guard_core.py @@ -247,6 +247,18 @@ def test_allows_forbidden_command_words_inside_gh_arguments(self) -> None: self.assertTrue(decision.allowed, decision.reason) + def test_denies_forbidden_commands_inside_shell_constructs(self) -> None: + root = Path("/tmp") + for command in ( + "if grep SymbolExtractor src; then echo yes; fi", + "while git grep SymbolExtractor; do break; done", + "until find . -name '*.cs'; do break; done", + ): + with self.subTest(command=command): + decision = core.evaluate_bash_command(command, cwd=root, project_root=root) + + self.assertFalse(decision.allowed) + def test_denies_quote_concatenated_high_risk_commands(self) -> None: root = Path("/tmp") for command in (