Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
af28cf3
fix(analyst): keep write denies after config grants and allow reading…
hkaltai Oct 26, 2026
7ae5a88
refactor(analyst): describe the post-config write denies in the marke…
hkaltai Oct 26, 2026
3a6f72d
fix(analyst): deny every built-in write tool after config, not just f…
hkaltai Oct 26, 2026
5857f49
fix(analyst): drop the task tool when config denies all delegation
hkaltai Oct 26, 2026
45f6da7
docs(analyst): state exactly what the permission examples deny
hkaltai Oct 26, 2026
a86cb90
fix(permission): check the symlink-resolved directory for external_di…
hkaltai Oct 26, 2026
4555a8b
docs(security): note that external_directory checks follow symlinks
hkaltai Oct 26, 2026
6d5378f
fix(permission): resolve symlinks for glob and ls, new write targets …
hkaltai Oct 26, 2026
a16d5db
fix(permission): follow dangling links and decide containment on the …
hkaltai Oct 26, 2026
698012c
fix(permission): bound dangling-link resolution by one shared link bu…
hkaltai Oct 26, 2026
6cbc8c4
test(filesystem): leave headroom in the link-chain time bound
hkaltai Oct 26, 2026
e5beb92
docs(permission): put wildcard rules first in examples and describe o…
hkaltai Oct 26, 2026
93685b3
docs(analyst): name what analyst keeps instead of calling it read-only
hkaltai Oct 26, 2026
5c883ad
fix(llm): preserve internal fallback for denied tool calls
hkaltai Oct 26, 2026
05de6a3
fix(permission): protect authored external paths
hkaltai Oct 26, 2026
c048b7d
fix(permission): close resolved-path bypasses
hkaltai Oct 26, 2026
35d03fe
fix(permission): preserve path and fallback identity
hkaltai Oct 26, 2026
f48cac9
docs(analyst): document scratch table exception
hkaltai Oct 26, 2026
803511c
chore(upstream): guard wildcard path changes
hkaltai Oct 26, 2026
5a4baca
fix(permission): harden tool name repair
hkaltai Oct 26, 2026
6b14216
fix(llm): reject inherited tool names
hkaltai Oct 26, 2026
a0b1eea
fix(permission): let resolved twins only tighten, and keep project fi…
hkaltai Oct 27, 2026
8eedd7e
chore(analyst): close one marker block around the permission log, and…
hkaltai Oct 27, 2026
e433449
fix(permission): check a symlink-resolved path without rewriting conf…
hkaltai Oct 27, 2026
489d22c
fix(permission): resolve path rules under a wildcard permission too
hkaltai Oct 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions docs/docs/configure/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Agents define different AI personas with specific models, prompts, permissions,
| Agent | Description | Access Level |
|-------|------------|-------------|
| `builder` | Create and modify dbt models, SQL pipelines, and data transformations | Full read/write. SQL mutations prompt for approval. |
| `analyst` | Answer questions about your data — explore it, run SELECT queries, inspect schemas, generate insights | Read-only (enforced). SQL writes denied. Safe bash commands auto-allowed. |
| `analyst` | Answer questions about your data — explore it, run SELECT queries, inspect schemas, generate insights | Read-only (enforced). SQL writes, file edits, shell, memory saves and other built-in write tools denied, even if config allows them. MCP and custom tools follow your config. Training entries and session scratch tables are still allowed. |
| `plan` | Plan before acting — restricted to planning files only | Minimal — no edits, no bash, no SQL |

### Builder
Expand All @@ -25,10 +25,11 @@ The agent for asking questions about your data. Truly read-only mode for safe da

- **File access**: Read, grep, glob — no prompts
- **SQL**: SELECT queries execute freely. Write queries are denied (not prompted — blocked entirely)
- **Bash**: Safe commands auto-allowed (`ls`, `grep`, `cat`, `head`, `tail`, `find`, `wc`). dbt read commands allowed (`dbt list`, `dbt ls`, `dbt debug`). Everything else denied.
- **Bash**: Denied. Use read, grep and glob to read files
- **Web**: Fetch and search allowed without prompts
- **Schema/warehouse/finops**: All inspection tools available
- **Workspace knowledge**: Loads workspace and project skills; reads workspace memory and views this checkout's memory audit log. Reloading memory syncs the local copy with the workspace, which can remove a local copy of a block the workspace archived. Saving memory is done with `builder`.
- **Workspace knowledge**: Loads workspace and project skills; reads workspace memory and views this checkout's memory audit log. Can read files bundled with skills, including global skills stored outside the project. Reloading memory syncs the local copy with the workspace, which can remove a local copy of a block the workspace archived. Saving memory is done with `builder`.
- **Config overrides**: A permission config such as `"*": "allow"` cannot turn file edits, SQL writes (including data diffs of arbitrary queries), shell, memory saves, warehouse connection or MCP config changes, training imports, sample project setup, feedback submission, batch tool calls or delegation beyond `data-explorer` back on. A global `"*": "allow"` does turn on MCP and custom tools for analyst, including ones that write (for example a SQL MCP server); to keep them from analyst, put the `"*": "allow"` under `agent.builder` instead of the global config, or deny those tools under `agent.analyst.permission`.

!!! tip
Use `analyst` when exploring data to ensure no accidental writes. Switch to `builder` when you're ready to create or modify models.
Expand Down Expand Up @@ -66,8 +67,8 @@ Define custom agents in `altimate-code.json`:
"write": "deny",
"edit": "deny",
"bash": {
"dbt docs generate": "allow",
"*": "deny"
"*": "deny",
"dbt docs generate": "allow"
}
}
}
Expand Down Expand Up @@ -121,7 +122,7 @@ Each agent can have its own permission overrides:
```json
{
"agent": {
"analyst": {
"builder": {
"permission": {
"write": "deny",
"edit": "deny",
Expand All @@ -137,6 +138,8 @@ Each agent can have its own permission overrides:
}
```

`analyst` already denies its shell and built-in write tools whatever its config says, so its config is mainly useful to restrict it further (for example `"webfetch": "deny"`) or to allow MCP and custom tools.

!!! warning
Agent-specific permissions override global permissions. A `"deny"` at the agent level cannot be overridden by a global `"allow"`.

Expand Down
21 changes: 9 additions & 12 deletions docs/docs/configure/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,21 +65,25 @@ Override permissions for specific agents:
```json
{
"agent": {
"analyst": {
"builder": {
"permission": {
"write": "deny",
"edit": "deny",
"bash": {
"*": "deny",
"SELECT *": "allow",
"dbt docs *": "allow",
"*": "deny"
"dbt docs *": "allow"
}
}
}
}
}
```

The `analyst` agent's shell and built-in write tools (file edits, SQL writes, memory saves and the other built-in tools that change state) stay denied even if its permission config allows them. Training entries remain available through `training_save` and `training_remove`, and analyst can create session scratch tables. MCP and custom tools follow your config, so a global `"*": "allow"` turns them on for analyst too, including ones that write. Config can still restrict analyst further.

Rules are evaluated in order and the last matching rule wins, so put a `"*"` rule before the more specific patterns it should not override.

## All Permissioned Tools

| Tool | Supports Patterns | Description |
Expand Down Expand Up @@ -209,15 +213,8 @@ Give each agent only the permissions it needs:
"agent": {
"analyst": {
"permission": {
"write": "deny",
"edit": "deny",
"sql_execute_write": "deny",
"bash": {
"*": "deny",
"ls *": "allow",
"cat *": "allow",
"dbt list *": "allow"
}
"webfetch": "deny",
"websearch": "deny"
}
},
"builder": {
Expand Down
18 changes: 9 additions & 9 deletions docs/docs/configure/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ Control which tools agents can use via the [permission system](permissions.md):
{
"permission": {
"bash": {
"*": "ask",
"dbt *": "allow",
"rm *": "deny",
"*": "ask"
"rm *": "deny"
},
"write": "ask",
"read": "allow"
Expand All @@ -58,22 +58,22 @@ Disable a tool for a specific agent by setting its permission to `"deny"`:
```json
{
"agent": {
"analyst": {
"builder": {
"permission": {
"write": "deny",
"edit": "deny",
"bash": {
"dbt run *": "deny",
"*": "ask"
"*": "ask",
"dbt run *": "deny"
}
}
}
}
}
```

!!! example "Read-only analyst"
The configuration above creates an analyst agent that cannot modify files. It can only read and explore the codebase, and must ask before running shell commands (except `dbt run`, which is blocked entirely).
!!! example "Builder with file edit tools denied"
The configuration above denies the builder agent's `write` and `edit` tools. The builder can read and explore the codebase. It must ask before running other shell commands, and `dbt run` is blocked entirely. An approved shell command can still modify files. The `analyst` agent's shell and built-in write tools are already denied whatever its permission config says. It can still save training entries and create session scratch tables for analysis, and MCP or custom tools follow your config.

## Tool Behavior

Expand All @@ -85,11 +85,11 @@ The `bash` tool executes shell commands in the project directory. Commands run i
{
"permission": {
"bash": {
"*": "ask",
"dbt *": "allow",
"git *": "allow",
"python *": "allow",
"rm -rf *": "deny",
"*": "ask"
"rm -rf *": "deny"
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion docs/docs/data-engineering/agent-modes.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,10 @@ Analyst mode enforces strict guardrails:

- **SELECT only.** INSERT, UPDATE, DELETE, DROP are denied (not prompted, blocked entirely)
- **No file writes.** Cannot modify your codebase
- **Safe bash only.** `ls`, `grep`, `cat`, `head`, `tail`, `find`, `wc` auto-allowed. dbt read commands allowed (`dbt list`, `dbt ls`, `dbt debug`, `dbt deps`). Everything else denied.
- **No shell.** Bash is denied. Files are read with the read, grep and glob tools
- **No memory saves.** Workspace memory blocks can be read, but not saved, extracted or deleted (use builder). Training entries can still be saved and removed
- **Config cannot lift these.** A permission config such as `"*": "allow"` does not turn file edits, SQL writes (including data diffs of arbitrary queries), shell, memory saves, warehouse connection or MCP config changes, training imports, sample project setup, feedback submission, batch tool calls or delegation beyond `data-explorer` back on for analyst. MCP and custom tools are not covered: a global `"*": "allow"` turns them on for analyst too, including ones that write.
- **Skill files readable.** Files bundled with a skill, including global skills stored outside the project, can be read
- **Full web access.** Fetch and search allowed without prompts
- **All inspection tools.** Schema, warehouse, finops, lineage, and core analysis tools available

Expand Down
29 changes: 13 additions & 16 deletions docs/docs/getting-started/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,11 @@ Verify your connection:

altimate ships with specialized agent modes, each with its own tool permissions:

| Mode | Access | Use when you want to... |
| ----------- | ---------- | ------------------------------------------------------------------------------ |
| **Builder** | Read/Write | Create and modify SQL, dbt models, pipelines. SQL writes prompt for approval. |
| **Analyst** | Read-only | Ask questions about your data, explore production data safely, run cost analysis. SQL writes denied entirely. |
| **Plan** | Minimal | Plan an approach before switching to builder to execute it |
| Mode | Access | Use when you want to... |
| ----------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Builder** | Read/Write | Create and modify SQL, dbt models, pipelines. SQL writes prompt for approval. |
| **Analyst** | Read-only | Ask questions about your data, explore production data safely, run cost analysis. Production SQL writes are denied; session scratch tables are allowed. |
| **Plan** | Minimal | Plan an approach before switching to builder to execute it |

Switch modes in the TUI:

Expand Down Expand Up @@ -234,12 +234,7 @@ Set tool permissions for each agent mode in `altimate-code.json`:
"agent": {
"analyst": {
"permission": {
"write": "deny",
"edit": "deny",
"bash": {
"dbt docs generate": "allow",
"*": "deny"
}
"webfetch": "deny"
}
},
"builder": {
Expand All @@ -256,6 +251,8 @@ Set tool permissions for each agent mode in `altimate-code.json`:
}
```

Analyst's shell and built-in write tools are always denied, so config cannot turn them on. It can still save training entries and create session scratch tables, and MCP or custom tools follow your config.

### Project rules with AGENTS.md

Define project-wide conventions in an `AGENTS.md` file at your project root. These rules are automatically loaded into every agent's system prompt:
Expand All @@ -271,11 +268,11 @@ Define project-wide conventions in an `AGENTS.md` file at your project root. The

### Default permissions by agent mode

| Agent | File writes | SQL writes | Bash | Training |
| ------- | ----------- | -------------------------- | --------------------------------- | -------- |
| Builder | allow | ask (prompts for approval) | ask | allow |
| Analyst | deny | deny (blocked entirely) | deny (safe commands auto-allowed) | allow |
| Plan | deny | deny | deny | deny |
| Agent | File writes | SQL writes | Bash | Training |
| ------- | ----------- | --------------------------- | ---- | -------- |
| Builder | allow | ask (prompts for approval) | ask | allow |
| Analyst | deny | deny, except scratch tables | deny | allow |
| Plan | deny | deny | deny | deny |

---

Expand Down
12 changes: 6 additions & 6 deletions docs/docs/reference/security-faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,18 +96,18 @@ Yes. Use pattern-based permissions to deny destructive SQL:
}
```

You can also configure per-agent permissions. For example, restrict the `analyst` agent to read-only:
You can also configure per-agent permissions. For example, keep the `builder` agent from editing files and allow only `SELECT` shell commands. The `analyst` agent's shell and built-in write tools are already denied whatever its config says. It can still save training entries and create session scratch tables, and MCP or custom tools follow your config.

```json
{
"agent": {
"analyst": {
"builder": {
"permission": {
"write": "deny",
"edit": "deny",
"bash": {
"SELECT *": "allow",
"*": "deny"
"*": "deny",
"SELECT *": "allow"
}
}
}
Expand Down Expand Up @@ -300,8 +300,8 @@ For additional safety:

Altimate Code includes several layers of protection to keep the agent within your project:

- **Project boundary enforcement.** File operations check that paths stay within your project directory (or git worktree for monorepos). Attempts to read or write outside the project trigger an `external_directory` permission prompt.
- **Symlink-aware path resolution.** Symlinks inside the project that point outside are detected and blocked. This prevents an agent from reading or writing outside your project through symlinks.
- **Project boundary enforcement.** File operations check that paths stay within your project directory (or git worktree for monorepos). Attempts to read or write outside the project trigger an `external_directory` permission prompt. A path that goes through a symlink is also checked against the directory it resolves to, so a symlink inside an allowed directory cannot be used to reach a directory outside it that is not allowed.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor (not blocking): a read through an allowed link into a subfolder that an earlier rule or layer denies is still allowed, and no test or doc pins this

Config: global external_directory: {"<root>/real/secret/*": "deny"} plus agent.<name>.permission.external_directory: {"<root>/link/*": "allow"}, with link -> real. The same happens with both rules in one map, deny first. A read of link/secret/f sends [link/secret/*, real/secret/*]. The written glob is allowed by link/*, and the resolved glob is allowed by the link/* resolved spelling that follows the deny, so the result is allow.

This is not a regression. Main allows the same request because it asks only for the written glob. The same rules written without a link (real/secret/*: deny, then real/*: allow) also allow under last-match-wins. Making the earlier narrower deny win here is the ordering approach that broke the deny-volume/allow-project carve-out last round. But the agent-level test (analyst-data-explorer.test.ts:625-657) asserts only the direct read and real/other, while its comment says the deny "must survive a per-agent allow written through a link". This sentence can also be read as promising that a deny is enforced through any link.

Suggestion: pin the intended decision with decide(layered, glob("link", "secret"), glob("real", "secret")) (expect allow) in next.test.ts and in the agent test. Add one sentence here, along these lines: "An allow for a symlinked folder also covers the folder it resolves to, so put a deny for one of its subfolders after that allow." This is safe to do in a follow-up.

- **Symlink-aware path resolution.** Symlinks inside the project that point outside are resolved, so following one triggers the same `external_directory` permission check as any other path outside the project. Whether it is allowed depends on that permission.
- **Path traversal blocking.** Paths containing `../` sequences that would escape the project are rejected with an "Access denied" error.
- **Sensitive file protection.** Writing to credential files (`.env`, `.ssh/`, `.aws/`, private keys) triggers a confirmation prompt, even inside the project. See [below](#why-am-i-being-prompted-to-edit-env-files) for details.
- **Bash command analysis.** The bash tool parses commands with tree-sitter to detect file operations (`rm`, `cp`, `mv`, etc.) targeting paths outside your project, and prompts for permission.
Expand Down
15 changes: 12 additions & 3 deletions packages/core/src/util/wildcard.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
export * as Wildcard from "./wildcard"

// altimate_change start — preserve POSIX backslashes when matching filesystem paths
export function match(input: string, pattern: string) {
const normalized = input.replaceAll("\\", "/")
return matchNormalized(input.replaceAll("\\", "/"), pattern.replaceAll("\\", "/"))
}

export function matchPath(input: string, pattern: string) {
if (process.platform === "win32") return match(input, pattern)
return matchNormalized(input, pattern)
}

function matchNormalized(input: string, pattern: string) {
let escaped = pattern
.replaceAll("\\", "/")
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
.replace(/\*/g, ".*")
.replace(/\?/g, ".")

if (escaped.endsWith(" .*")) escaped = escaped.slice(0, -3) + "( .*)?"

return new RegExp("^" + escaped + "$", process.platform === "win32" ? "si" : "s").test(normalized)
return new RegExp("^" + escaped + "$", process.platform === "win32" ? "si" : "s").test(input)
}
// altimate_change end
43 changes: 41 additions & 2 deletions packages/opencode/src/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,9 @@ export const layer = Layer.effect(
question: "allow",
// Skills return instruction text only (no side effects), so they are safe for a read-only agent.
skill: "allow",
// "*": "deny" above also overrides the default allow for skill directories; add it back
// so files bundled with a global skill (stored outside the project) stay readable.
external_directory: Object.fromEntries(skillDirs.map((dir) => [path.join(dir, "*"), "allow"])),
// Workspace memory: read, reload and view the audit log. Reloading syncs like the
// load every session runs (it can drop a local copy of a block the workspace has
// archived), but cannot create or edit memory. Saving and deleting stay denied, so a
Expand Down Expand Up @@ -633,12 +636,48 @@ export const layer = Layer.effect(
// Ensure Truncate.GLOB is allowed unless explicitly configured
for (const name in agents) {
const agent = agents[name]
// altimate_change start — keep the analyst shell disabled after config and safety rules
// altimate_change start — keep analyst's shell and write tools disabled after config and safety rules
// Permission.disabled requires the final bash rule to deny "*". Without
// it, narrower safety rules leave the tool exposed to commands that emit
// no permission patterns (e.g. `>out.txt`). data-explorer is capped above.
// The built-in tools that change files, memory, config, connections or external
// systems are denied again too: last-match-wins would otherwise let a config grant
// such as "*": "allow" turn them back on for the read-only agent. Most of them never

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor (earlier, deferred, not blocking): a global "*": "allow" still turns on MCP and custom/plugin tools for analyst, including ones that write

The post-config re-deny below covers built-in tools only. This is deliberate. It is documented on agents.md, permissions.md and agent-modes.md with two workarounds, and listed in the PR follow-ups, so it is safe to address in a follow-up.

// call ctx.ask, so removing them from the tool list is their only gate. batch is
// denied because it runs other tools without that tool-list filter.
if (name === "analyst") {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: a global "*": "allow" still enables MCP and custom/plugin write tools for analyst

These post-config denies cover built-in tools only. A global permission: {"*": "allow"} written for builder (the example in #1596) therefore still turns on every MCP and custom tool for analyst, including ones that write (SQL MCP servers, GitHub, ticketing). The docs now say "MCP and custom tools follow your config", so I read this as deliberate. Even so, analyst's description ("Cannot modify files or run destructive SQL") no longer holds under the most common permissive config.

Suggestion: for analyst, ignore wildcard-only grants for non-built-in tools, and honour only grants that name the tool (or MCP server prefix) or come from agent.analyst.permission. If that is out of scope, have the docs say explicitly that a global "*": "allow" exposes MCP write tools to analyst.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You read it right: it is deliberate. AI-10226 rejected an allow-list cap so that MCP and custom tools a team adds to analyst keep working, and the post-config deny covers built-ins only. I agree the docs have to say plainly what a global "*": "allow" does, so in a0b1eea agents.md, agent-modes.md and permissions.md now state that it also turns on MCP and custom tools for analyst, including ones that write. agents.md also gives the two ways to avoid that: put the wildcard under agent.builder instead of the global config, or deny those tools under agent.analyst.permission.

Ignoring wildcard-only grants for non-built-in tools would change how config precedence works for analyst, so I have not done that in this PR. It is listed as a follow-up in the PR description.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor (non-blocking, reasonably deferred): a global "*": "allow" still turns on MCP and custom write tools for analyst.

There is no code change here, and that's fine. It's deliberate, now documented on all three docs pages with the two workarounds, and listed as a follow-up in the PR description. I'm noting it so the open item stays visible. It does not block this PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, agreed. It stays deferred, is documented on the three pages with both workarounds, and is in the PR follow-ups.

agent.permission = Permission.merge(agent.permission, Permission.fromConfig({ bash: "deny" }))
// Keep what config resolved for data-explorer (it may deny or ask), but nothing broader.
const dataExplorerAction = Permission.evaluate("task", "data-explorer", agent.permission).action
agent.permission = Permission.merge(
agent.permission,
Permission.fromConfig({
...Object.fromEntries(
[
"bash",
"sql_execute_write",
"edit",
"write",
"apply_patch",
"altimate_memory_write",
"altimate_memory_delete",
"altimate_memory_extract",
"training_import",
"warehouse_add",
"warehouse_remove",
"warehouse_install_driver",
"mcp_discover",
"datamate_manager",
"sample_setup",
"feedback_submit",
// Runs caller-supplied SQL without the write classification sql_execute applies.
"data_diff",
"batch",
].map((tool) => [tool, "deny" as const]),
),
// A trailing "*" deny is what removes task from the tool list when nothing is allowed.
task: dataExplorerAction === "deny" ? "deny" : { "*": "deny", "data-explorer": dataExplorerAction },
}),
)
}
// altimate_change end
const explicit = agent.permission.some((r) => {
Expand Down
Loading
Loading