mirror of
https://github.com/expo/code-review-cli.git
synced 2026-11-14 09:58:11 -10:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75fccfe3f4 | ||
|
|
621c354568 | ||
|
|
3af45ebf84 |
No files matched your search
@@ -3,7 +3,7 @@ description: Security and secrets. Injection, credential or secret leakage, unsa
|
||||
alwaysRun: true
|
||||
# Security is the highest-stakes agent, so it runs on the Opus tier (included in
|
||||
# the Max/Team subscription the claude-code engine authenticates with).
|
||||
model: anthropic/claude-opus-5
|
||||
model: anthropic/claude-opus-5-5
|
||||
---
|
||||
|
||||
# Security & secrets
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
// Default model for every agent. Override per-agent via frontmatter in the
|
||||
// agent's markdown, or at runtime with the REVIEWER_MODEL env var
|
||||
// (e.g. REVIEWER_MODEL=anthropic/claude-haiku-4-5).
|
||||
"model": "anthropic/claude-sonnet-5",
|
||||
"model": "anthropic/claude-opus-5-5",
|
||||
|
||||
// Agents: every markdown file in agents/ is one reviewer (id = filename).
|
||||
// Add or remove files to change the roster — no list needed here.
|
||||
// shared.md (prepended to every agent + coordinator) and coordinator.md are
|
||||
// reserved filenames. Per-agent overrides go in each file's YAML frontmatter,
|
||||
// e.g. `---\nmodel: anthropic/claude-opus-5\n---`.
|
||||
// e.g. `---\nmodel: anthropic/claude-opus-5-5\n---`.
|
||||
|
||||
"policy": {
|
||||
// Phase 1: keep signal high by surfacing only critical/warning.
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
# like `opencode-ai` in package.json — an unpinned CLI would silently drift
|
||||
# on every CI run; bump it deliberately.
|
||||
- name: Install Claude Code CLI
|
||||
run: bun install -g @anthropic-ai/[email protected]12
|
||||
run: bun install -g @anthropic-ai/[email protected]80
|
||||
|
||||
# SECURITY: the TRUSTED BASE checkout above includes .expo-code-review/
|
||||
# config.jsonc, whose auth.tokenEnv names the env var the CLI forwards as the
|
||||
|
||||
@@ -328,7 +328,7 @@ your-monorepo/
|
||||
// apps/api/.expo-agents/code-review/config.jsonc (the api team owns this)
|
||||
{
|
||||
// NO "auth" block — locked centrally; a tokenEnv here is rejected by loader + CI guard.
|
||||
"model": "anthropic/claude-sonnet-5",
|
||||
"model": "anthropic/claude-opus-5-5",
|
||||
"policy": { "includeSuggestions": false },
|
||||
"noise": { "additionalIgnores": ["apps/api/**/__generated__/**"] }
|
||||
// shared.md, coordinator.md, agents/*.md live beside this file — the api team's roster.
|
||||
@@ -475,7 +475,7 @@ cacheable size (~1–4K tokens) show `cache read 0` — expected, not a bug.
|
||||
---
|
||||
description: One line the router uses to decide relevance.
|
||||
alwaysRun: true # run even when the router would skip this agent
|
||||
model: anthropic/claude-opus-5 # override the default model
|
||||
model: anthropic/claude-haiku-4-5 # override the default model
|
||||
temperature: 0.1
|
||||
---
|
||||
|
||||
@@ -491,7 +491,7 @@ per-repo `noise.additionalIgnores`.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"model": "anthropic/claude-sonnet-5", // default model for the specialists
|
||||
"model": "anthropic/claude-opus-5-5", // default model for the specialists
|
||||
"policy": { "includeSuggestions": false }, // suppress suggestion-severity findings
|
||||
"chunk": { "maxChangedLines": 1000, "maxFiles": 20 }, // concurrency defaults: 6 (API key) / 3 (subscription)
|
||||
"noise": { "additionalIgnores": ["packages/*/build/**"] },
|
||||
@@ -514,15 +514,17 @@ Precedence: **`REVIEWER_MODEL` env** (global override) → per-file **frontmatte
|
||||
`model:`** → **`config.jsonc` `model`** (the default). So a repo can run a mixed
|
||||
setup, and a developer can override everything locally.
|
||||
|
||||
- **Specialist agents** (correctness/security/consistency) benefit from a
|
||||
reasoning-tier model — **`anthropic/claude-sonnet-5`** is the quality/speed
|
||||
sweet spot (the scaffolded default). The **Opus tier** finds more but is slower
|
||||
and more expensive, so scope it to the highest-stakes agent: **security runs on
|
||||
`anthropic/claude-opus-5`** (set in `security.md` frontmatter), the rest on the
|
||||
default.
|
||||
- **Specialist agents** (correctness/security/consistency) run on
|
||||
**`anthropic/claude-opus-5-5`**, the scaffolded default. It finds more than the
|
||||
Sonnet tier but is slower and costs about twice as much per token, so
|
||||
`anthropic/claude-sonnet-5` is the step down when a repo needs faster or cheaper
|
||||
reviews. **Security stays pinned to `anthropic/claude-opus-5-5`** in `security.md`
|
||||
frontmatter, so a lower default does not move the highest-stakes agent.
|
||||
- **The coordinator** makes the final call (dedupe / re-judge / decide) — worth a
|
||||
strong model; the scaffold pins it to `anthropic/claude-opus-5` in
|
||||
strong model; the scaffold pins it to `anthropic/claude-opus-5-5` in
|
||||
`coordinator.md` frontmatter.
|
||||
- **Opus 5.5 needs Claude Code 2.1.280 or newer.** Older CLIs reject it with a 400,
|
||||
so keep the workflow's `@anthropic-ai/claude-code` pin at 2.1.280 or later.
|
||||
- If latency/timeouts dominate on big PRs, moving the specialists to a faster model
|
||||
(e.g. `anthropic/claude-haiku-4-5`) is the most direct lever (a real recall
|
||||
tradeoff — measure it).
|
||||
|
||||
@@ -174,12 +174,22 @@ reads are denied by the unmatched-rule denial, and a withheld tool is denied by
|
||||
([observed] `claude-code.ts:241-321`; behavior verified against `claude 2.1.212`).
|
||||
Write/exec/network tools (`Bash`, `Edit`, `Write`, `NotebookEdit`, `WebFetch`,
|
||||
`WebSearch`, `Task`, …) are always denied for every pass regardless of role, via the
|
||||
`ALWAYS_DENIED_TOOLS` enumeration. A deny **enumeration** is used rather than an
|
||||
allow-only model because permission rules cannot fail closed for tools outside the read
|
||||
set and a `*` deny breaks tool calling outright — both verified against `claude 2.1.212`.
|
||||
The residual (a future CLI version shipping a new read-capable tool this list does not
|
||||
name) is bounded by pinning the CLI version, so **the list must be revisited on every
|
||||
pinned-version bump** ([observed] `claude-code.ts:86-111`).
|
||||
`ALWAYS_DENIED_TOOLS` enumeration. Permission rules alone cannot fail closed for tools
|
||||
outside the read set, and a `*` deny breaks tool calling outright — both verified
|
||||
against `claude 2.1.212`.
|
||||
|
||||
**`--tools` is the outer bound.** `--tools` is not a permission rule: it names the
|
||||
built-in tools the CLI **loads**. ecr passes exactly the granted read tools
|
||||
(`Read,Grep,Glob` or fewer), and `""` for the coordinator and the no-tools fallback.
|
||||
A tool that is not loaded cannot be called, so a new tool in a future CLI version
|
||||
stays out without anyone adding its name. The deny list was not enough on its own:
|
||||
against `claude 2.1.280`, `dontAsk` rejected `Monitor` and `Workflow`, but
|
||||
`EnterWorktree` created a git worktree, `RemoteTrigger` listed the logged-in
|
||||
account's cloud routines, and `SendMessage` and `Skill` ran, because those tools ask
|
||||
no permission. MCP tools (the research tools) and the CLI's `StructuredOutput` tool
|
||||
(used by `--json-schema`) are outside the built-in set and stay loaded — also verified
|
||||
against `2.1.280`. The scoped allow rules and `ALWAYS_DENIED_TOOLS` stay as inner
|
||||
layers ([observed] `claude-code.ts` `buildClaudeArgs`, `ALWAYS_DENIED_TOOLS`).
|
||||
|
||||
**The env is an allowlist.** The child env is built from `CHILD_ENV_ALLOWLIST` (PATH,
|
||||
HOME, locale, tmp, proxies, config dirs — what a CLI needs and nothing else), never a
|
||||
|
||||
@@ -245,6 +245,9 @@ test("buildClaudeArgs: read-only, trust-isolated, subscription argv; never --bar
|
||||
);
|
||||
expect(joined).not.toContain(" Grep ");
|
||||
expect(joined).not.toContain(" Glob ");
|
||||
// --tools loads only the granted read tools, so an ungranted built-in (a new one
|
||||
// in a future CLI included) never reaches the model.
|
||||
expect(args[args.indexOf("--tools") + 1]).toBe("Read,Grep,Glob");
|
||||
// Write/exec/net tools are denied outright; out-of-tree reads are denied by
|
||||
// dontAsk's unmatched-rule denial (NO explicit Read deny: `Read(~/**)` would
|
||||
// deny the whole tree whenever the repo lives under the home directory).
|
||||
@@ -582,6 +585,7 @@ test("buildClaudeArgs: cross-cutting/verifier get Read+Grep scoped, Glob denied
|
||||
// withheld read tool must be denied BY NAME, not just left out of --allowedTools.
|
||||
expect(joined).toContain("--allowedTools Read(//work/repo/**) Grep(//work/repo/**)");
|
||||
expect(joined).not.toContain("Glob(/");
|
||||
expect(args[args.indexOf("--tools") + 1]).toBe("Read,Grep");
|
||||
expect(joined).toContain(
|
||||
"--disallowedTools Glob Bash Edit Write NotebookEdit NotebookRead WebFetch WebSearch Task TodoWrite BashOutput KillShell ExitPlanMode",
|
||||
);
|
||||
@@ -599,9 +603,9 @@ test("buildClaudeArgs: reviewer tools honor the configured read set (list ignore
|
||||
"--allowedTools Read(//work/repo/**) Grep(//work/repo/**) Glob(//work/repo/**)",
|
||||
);
|
||||
// `list` has no scoped Claude equivalent and is silently ignored (no crash, no rule).
|
||||
expect(joined).not.toContain("List");
|
||||
expect(args.some((arg) => arg.startsWith("List("))).toBe(false);
|
||||
expect(joined).toContain(
|
||||
"--disallowedTools Bash Edit Write NotebookEdit NotebookRead WebFetch WebSearch Task TodoWrite BashOutput KillShell ExitPlanMode",
|
||||
"--disallowedTools Bash Edit Write NotebookEdit NotebookRead WebFetch WebSearch Task TodoWrite BashOutput KillShell ExitPlanMode ListAgents",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -616,6 +620,8 @@ test("buildClaudeArgs: empty tools (coordinator/no-tools fallback) deny every re
|
||||
// No --allowedTools at all, and Read/Grep/Glob explicitly denied so dontAsk's
|
||||
// default-allow can't grant a read.
|
||||
expect(joined).not.toContain("--allowedTools");
|
||||
// An empty --tools value loads no built-in tool at all.
|
||||
expect(args[args.indexOf("--tools") + 1]).toBe("");
|
||||
expect(joined).toContain(
|
||||
"--disallowedTools Read Grep Glob Bash Edit Write NotebookEdit NotebookRead WebFetch WebSearch Task TodoWrite BashOutput KillShell ExitPlanMode",
|
||||
);
|
||||
@@ -643,6 +649,8 @@ test("buildClaudeArgs: explicit research MCP replaces safe mode without loading
|
||||
expect(joined).toContain("mcp__platform_docs__search_platform_docs");
|
||||
expect(joined).toContain("mcp__platform_docs__fetch_platform_doc");
|
||||
expect(joined).toContain("--mcp-config /private/mcp.json");
|
||||
// MCP tools sit outside the built-in set, so --tools does not remove them.
|
||||
expect(args[args.indexOf("--tools") + 1]).toBe("Read");
|
||||
expect(joined).toContain("--setting-sources --disable-slash-commands");
|
||||
expect(joined).toContain("--strict-mcp-config");
|
||||
expect(joined).toContain("--no-session-persistence");
|
||||
|
||||
@@ -89,9 +89,17 @@ test("inlineStubBody: neutral — never claims resolved or dismissed", () => {
|
||||
|
||||
// ---- oneLineRationale ----
|
||||
|
||||
test("oneLineRationale: collapses whitespace, escapes <, truncates at a word boundary", () => {
|
||||
test("oneLineRationale: strips HTML tags, collapses whitespace, escapes <, truncates at a word boundary", () => {
|
||||
expect(oneLineRationale("a\n\nb c")).toBe("a b c");
|
||||
expect(oneLineRationale("<details>x</details>")).toBe("<details>x</details>");
|
||||
expect(oneLineRationale("<details>x</details>")).toBe("x");
|
||||
expect(oneLineRationale("High — reason.<br>**Impact:** Medium<BR/>end")).toBe(
|
||||
"High — reason. **Impact:** Medium end",
|
||||
);
|
||||
expect(oneLineRationale("<details><summary>Why</summary>\n\nbody</details>")).toBe("Why body");
|
||||
// Non-formatting angle brackets stay, escaped.
|
||||
expect(oneLineRationale("returns `Promise<void>` when a < b")).toBe(
|
||||
"returns `Promise<void>` when a < b",
|
||||
);
|
||||
const long = Array.from({ length: 60 }, (_, i) => `word${i}`).join(" ");
|
||||
const short = oneLineRationale(long);
|
||||
expect(short.length).toBeLessThanOrEqual(161);
|
||||
|
||||
+21
-8
@@ -92,17 +92,16 @@ const READ_TOOL_MAP: Record<string, "Read" | "Grep" | "Glob"> = {
|
||||
glob: "Glob",
|
||||
};
|
||||
const ALL_READ_TOOLS = ["Read", "Grep", "Glob"] as const;
|
||||
// @ref LLP 0003#claude-code-cli-containment [implements] — deny enumeration (not allow-only) because an empty/absent --allowedTools list default-ALLOWS reads; verified against claude 2.1.212, revisit on every CLI version bump
|
||||
// @ref LLP 0003#claude-code-cli-containment [implements] — inner deny layer behind --tools, because an empty/absent --allowedTools list default-ALLOWS reads; verified against claude 2.1.212, tool list re-checked against 2.1.280 (ListAgents added)
|
||||
/**
|
||||
* Tools never available to a review pass, whatever the role. A DENY enumeration is
|
||||
* the only workable containment: permission rules cannot fail closed here — reads
|
||||
* Tools never available to a review pass, whatever the role. The primary bound is
|
||||
* `--tools` (see buildClaudeArgs), which loads only the granted read tools; this
|
||||
* list is the inner layer behind it. Permission rules alone cannot fail closed — reads
|
||||
* inside the workspace are default-ALLOWED even when an allow list is present but
|
||||
* unmatched, and a `*` deny breaks tool calling outright (both verified against
|
||||
* claude 2.1.212). The residual risk — a FUTURE CLI version shipping a new
|
||||
* read-capable tool this list doesn't name — is bounded by pinning the CLI version
|
||||
* (the scaffolded workflow installs an exact @anthropic-ai/claude-code version;
|
||||
* bump it deliberately and revisit this list). Unknown names are ignored by the
|
||||
* CLI, so denying tools that don't exist in a given version is harmless.
|
||||
* claude 2.1.212). A FUTURE CLI version shipping a new tool this list doesn't name
|
||||
* is no longer the gap it was: `--tools` does not load it. Unknown names are ignored
|
||||
* by the CLI, so denying tools that don't exist in a given version is harmless.
|
||||
*/
|
||||
const ALWAYS_DENIED_TOOLS = [
|
||||
"Bash",
|
||||
@@ -117,6 +116,7 @@ const ALWAYS_DENIED_TOOLS = [
|
||||
"BashOutput",
|
||||
"KillShell",
|
||||
"ExitPlanMode",
|
||||
"ListAgents",
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -273,6 +273,16 @@ export function claudeModelMatches(requested: string, actualKey: string): boolea
|
||||
* tree itself (paths resolve to absolute), and `Read(~/**)` denies the whole tree
|
||||
* whenever the repo lives under the home directory — the common case.
|
||||
*
|
||||
* `--tools` is the outer bound: it names the built-in tools the CLI LOADS, so
|
||||
* everything else (including tools a future CLI version adds) never reaches the
|
||||
* model. It is an availability allowlist, not a permission rule, so it fails closed
|
||||
* where `--allowedTools` does not. Verified against claude 2.1.280: `dontAsk` does
|
||||
* NOT stop tools that ask no permission — EnterWorktree created a git worktree,
|
||||
* RemoteTrigger listed the account's cloud routines, SendMessage and Skill ran — and
|
||||
* `--tools` removes them all. MCP tools (the research tools) and the CLI's own
|
||||
* StructuredOutput tool (`--json-schema`) are not in the built-in set and stay
|
||||
* loaded. The scoped allow rules and ALWAYS_DENIED_TOOLS stay as the inner layers.
|
||||
*
|
||||
* This is NOT, by itself, a boundary against a symlink committed inside the PR-head
|
||||
* tree (e.g. `docs/notes.md -> ~/.claude/.credentials.json`): the permission rule
|
||||
* matches the literal path ARGUMENT, which is in-tree, but Read/Grep then follow the
|
||||
@@ -324,6 +334,9 @@ export function buildClaudeArgs(opts: {
|
||||
"--append-system-prompt",
|
||||
opts.system,
|
||||
...(opts.jsonSchema ? ["--json-schema", JSON.stringify(opts.jsonSchema)] : []),
|
||||
// @ref LLP 0003#claude-code-cli-containment [implements] — load only the granted read tools; "" loads none
|
||||
"--tools",
|
||||
enabled.join(","),
|
||||
...(allowedTools.length > 0 ? ["--allowedTools", ...allowedTools] : []),
|
||||
"--disallowedTools",
|
||||
...deniedReadTools,
|
||||
|
||||
+19
-6
@@ -385,14 +385,27 @@ function sourceLabel(value: string): string {
|
||||
const SHORT_RATIONALE_CHARS = 160;
|
||||
|
||||
/**
|
||||
* A rationale reduced to one plain line for the short (inlined) form: whitespace
|
||||
* collapsed, every `<` escaped so a truncated HTML block (`<details>`) can never
|
||||
* leave an unclosed tag in the comment, cut at a word boundary with an ellipsis.
|
||||
* This excerpt is the audit trail that survives the PR author collapsing or
|
||||
* resolving the inline thread, so it must always be non-empty when the rationale is.
|
||||
* HTML formatting tags a model writes into a rationale (`<br>`, `<details>`, ...).
|
||||
* A fixed list, not "anything in angle brackets", so a type like `Promise<void>`
|
||||
* in the prose survives.
|
||||
*/
|
||||
const FORMATTING_TAG_RE =
|
||||
/<\/?(?:a|b|blockquote|br|code|del|details|div|em|h[1-6]|hr|i|kbd|li|ol|p|pre|s|span|strong|sub|summary|sup|table|tbody|td|th|thead|tr|u|ul)\b[^<>]*>/gi;
|
||||
|
||||
/**
|
||||
* A rationale reduced to one plain line for the short (inlined) form: HTML
|
||||
* formatting tags removed (an escaped `<br>` would show as literal text),
|
||||
* whitespace collapsed, every remaining `<` escaped so a truncated HTML block can
|
||||
* never leave an unclosed tag in the comment, cut at a word boundary with an
|
||||
* ellipsis. This excerpt is the audit trail that survives the PR author collapsing
|
||||
* or resolving the inline thread, so it must always be non-empty when the rationale is.
|
||||
*/
|
||||
export function oneLineRationale(text: string): string {
|
||||
const flat = text.replace(/</g, "<").replace(/\s+/g, " ").trim();
|
||||
const flat = text
|
||||
.replace(FORMATTING_TAG_RE, " ")
|
||||
.replace(/</g, "<")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
if (flat.length <= SHORT_RATIONALE_CHARS) {
|
||||
return flat;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ alwaysRun: true
|
||||
# default model. Scoped to this one agent to limit the extra latency/rate-limit cost;
|
||||
# subdivide-on-timeout + the per-fetch deadline keep a slow Opus pass from hanging.
|
||||
# @ref LLP 0009#config-and-prompt-templates [implements]
|
||||
model: anthropic/claude-opus-5
|
||||
model: anthropic/claude-opus-5-5
|
||||
---
|
||||
|
||||
# Security & secrets
|
||||
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
# The anthropic/… models run through the Claude Code CLI (claude-code
|
||||
# engine). Pinned exactly — see workflow.yml (expo-code-review.yml).
|
||||
- name: Install Claude Code CLI
|
||||
run: npm install -g @anthropic-ai/[email protected]12
|
||||
run: npm install -g @anthropic-ai/[email protected]80
|
||||
|
||||
# SECURITY: the base-ref checkout includes every .expo-code-review/ config,
|
||||
# whose auth.tokenEnv names the forwarded model credential. `ecr verify-config`
|
||||
|
||||
@@ -115,7 +115,7 @@ jobs:
|
||||
# engine). Pinned exactly — see workflow.yml (expo-code-review.yml).
|
||||
- name: Install Claude Code CLI
|
||||
if: steps.cmd.outputs.run == 'true'
|
||||
run: npm install -g @anthropic-ai/[email protected]12
|
||||
run: npm install -g @anthropic-ai/[email protected]80
|
||||
|
||||
# SECURITY: the base-ref checkout above includes every .expo-code-review/
|
||||
# config.jsonc + routing.jsonc, whose auth.tokenEnv names the env var the CLI
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
// below for the credential. Override per-agent via frontmatter in the agent's
|
||||
// markdown, or at runtime with the REVIEWER_MODEL env var
|
||||
// (e.g. REVIEWER_MODEL=anthropic/claude-haiku-4-5).
|
||||
"model": "anthropic/claude-sonnet-5",
|
||||
"model": "anthropic/claude-opus-5-5",
|
||||
|
||||
// Agents: every markdown file in agents/ is one reviewer (id = filename).
|
||||
// Add or remove files to change the roster — no list needed here.
|
||||
// shared.md (prepended to every agent + coordinator) and coordinator.md are
|
||||
// reserved filenames. Per-agent overrides go in each file's YAML frontmatter,
|
||||
// e.g. `---\nmodel: anthropic/claude-opus-5\n---`.
|
||||
// e.g. `---\nmodel: anthropic/claude-opus-5-5\n---`.
|
||||
|
||||
// @ref LLP 0009#config-and-prompt-templates [implements] — suggestions off by default, not a schema limit
|
||||
"policy": {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# than the small serial-tail latency it adds (no repo tools, one bounded pass).
|
||||
# Override with a cheaper model if you'd rather trade decision quality for latency.
|
||||
# @ref LLP 0009#config-and-prompt-templates [implements]
|
||||
model: anthropic/claude-opus-5
|
||||
model: anthropic/claude-opus-5-5
|
||||
---
|
||||
|
||||
# Coordinator — consolidation & decision
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
{
|
||||
// Default model for every agent in this scope. Override per-agent via frontmatter,
|
||||
// or at runtime with REVIEWER_MODEL.
|
||||
"model": "anthropic/claude-sonnet-5",
|
||||
"model": "anthropic/claude-opus-5-5",
|
||||
|
||||
// Agents: every markdown file in agents/ beside this file is one reviewer for this
|
||||
// scope (id = filename). shared.md + coordinator.md are this scope's prompts.
|
||||
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
# would silently drift on every CI run; bump it deliberately. Remove this
|
||||
# step if you switch every model to an OpenCode-served provider.
|
||||
- name: Install Claude Code CLI
|
||||
run: npm install -g @anthropic-ai/[email protected]12
|
||||
run: npm install -g @anthropic-ai/[email protected]80
|
||||
|
||||
# SECURITY: the TRUSTED BASE checkout above includes every
|
||||
# .expo-code-review/config.jsonc + routing.jsonc, whose auth.tokenEnv names the
|
||||
|
||||
Reference in new issue
Block a user