Summary
VTCode automatically loads a workspace-root vtcode.toml. In the latest upstream main revision 8fef6b4d24fa42088203c999136000cbdfe62bb7 (the tested build reports version 0.143.4), and in the separately cross-checked 0.143.4 release source, the file can define hooks.lifecycle.session_start[].hooks[].command. When a user starts or resumes a normal VT Code Agent session, VT Code passes that value to sh -c with the workspace as the child current directory. The path does not require a model-generated tool call and does not perform a lifecycle-specific first-use approval or effective-command/content-change check.
This was dynamically reproduced with a harmless marker command in an isolated workspace. The configuration group created the marker; an otherwise identical workspace without vtcode.toml did not. The exact trigger is starting or resuming a VT Code Agent session in the workspace. This report does not claim that merely opening a directory without running VT Code is sufficient, and it does not include the separate status-line, custom-provider, Copilot, or Codex static paths as dynamically confirmed findings.
Impact
An attacker who can place or modify the effective workspace vtcode.toml can cause an arbitrary shell command to run with the privileges and inherited environment of the VT Code process when the victim starts or resumes a session in that workspace. The command runs from the workspace and can read or modify files available to the current user, access any permitted local resources, and launch further processes. The impact is local command execution, not privilege escalation.
Preconditions and Trust Boundary
The victim must run VT Code against the workspace and reach normal Agent session setup. The workspace configuration must contain a non-empty session_start hook command; lifecycle hooks are empty by default. A user who explicitly creates a hook has intentionally enabled this product feature. The boundary issue is that later repository-controlled configuration content can select or change the command without a new, understandable approval bound to the final command and referenced files.
Reproduction
Use a fresh temporary directory and build VT Code from main@8fef6b4d (the tested build reports version 0.143.4). Create a file named vtcode.toml with the following content:
[agent]
provider = "ollama"
[hooks.lifecycle]
quiet_success_output = true
[[hooks.lifecycle.session_start]]
[[hooks.lifecycle.session_start.hooks]]
command = "printf 'vtcode-session-start-marker\\n' > .vtcode-session-start-marker"
Start a normal session from that workspace:
vtcode --workspace /path/to/poc-workspace
After the session UI initializes, interrupt the session if no local provider is available, then inspect the marker:
test -f /path/to/poc-workspace/.vtcode-session-start-marker
test "$(cat /path/to/poc-workspace/.vtcode-session-start-marker)" = "vtcode-session-start-marker"
For the control group, use a second empty workspace without vtcode.toml and run the same binary and session startup, adding --provider ollama if needed to bypass first-run provider selection:
vtcode --workspace /path/to/control-workspace --provider ollama
test ! -e /path/to/control-workspace/.vtcode-session-start-marker
The marker command is intentionally limited to writing a fixed file inside the PoC workspace. It does not read credentials, contact a remote endpoint, alter shell startup files, or delete data.
Observed Result
On 0.143.4 built from 8fef6b4d24fa42088203c999136000cbdfe62bb7, the configured workspace created .vtcode-session-start-marker with the expected content during session setup. The no-configuration control did not create the marker. Both runs used fresh temporary home/config directories with no VTCODE_TRUST_WORKSPACE override or pre-existing trust record.
The test harness did not answer VT Code's terminal cursor-position query, so interrupting the TUI reported a cleanup error after startup. The marker was created before that error, and the same terminal limitation affected the control group.
Source Evidence
- Workspace discovery and configuration loading:
src/startup/config_loading.rs:20-66 resolves the workspace and builds the configuration; crates/codegen/vtcode-config/src/loader/manager.rs:128-168 adds the workspace-root vtcode.toml layer and :248-265 merges and validates the effective configuration.
- Hook configuration:
crates/codegen/vtcode-config/src/hooks.rs:18-32,132-145 defines session_start and accepts a command string with validation limited to type, non-empty content, matcher syntax, and timeout bounds.
- Session trigger:
src/agent/runloop/unified/session_setup/ui.rs:104-110 constructs LifecycleHookEngine from the effective hooks; :380-394 invokes run_session_start() during session UI setup.
- Process sink:
crates/codegen/vtcode-core/src/hooks/lifecycle/engine.rs:69-98 dispatches each matching command; :518-546 executes sh -c <configured command> in the workspace and calls spawn().
- Trust separation:
src/agent/runloop/unified/session_setup/init.rs:183-190,691-698 applies workspace trust to ToolRegistry prompt policy, while the lifecycle engine is constructed and executed through the separate path above.
Recommended Remediation
Before running a workspace-controlled lifecycle command, require an explicit per-workspace/per-launcher approval that displays the resolved shell command, working directory, and relevant environment policy. Bind the approval to the canonical workspace, repository revision or effective configuration digest, and the identity/content digest of referenced scripts. Revalidate those values immediately before every lifecycle spawn and after configuration reload. Do not reuse a tool-policy or full-auto trust bit as blanket approval for executable workspace configuration. Consider disabling workspace executable hooks by default or requiring a user-created local trust receipt before enabling them.
Acknowledgement
Thank you to @glmgbj233 for the detailed report, the careful dynamic reproduction, and the remediation suggestions that shaped the fix. Workspace-controlled lifecycle hooks now require explicit per-workspace approval of the exact command set before any command can run, and approvals are revalidated against the current configuration digest before every lifecycle spawn.
Summary
VTCode automatically loads a workspace-root
vtcode.toml. In the latest upstreammainrevision8fef6b4d24fa42088203c999136000cbdfe62bb7(the tested build reports version0.143.4), and in the separately cross-checked0.143.4release source, the file can definehooks.lifecycle.session_start[].hooks[].command. When a user starts or resumes a normal VT Code Agent session, VT Code passes that value tosh -cwith the workspace as the child current directory. The path does not require a model-generated tool call and does not perform a lifecycle-specific first-use approval or effective-command/content-change check.This was dynamically reproduced with a harmless marker command in an isolated workspace. The configuration group created the marker; an otherwise identical workspace without
vtcode.tomldid not. The exact trigger is starting or resuming a VT Code Agent session in the workspace. This report does not claim that merely opening a directory without running VT Code is sufficient, and it does not include the separate status-line, custom-provider, Copilot, or Codex static paths as dynamically confirmed findings.Impact
An attacker who can place or modify the effective workspace
vtcode.tomlcan cause an arbitrary shell command to run with the privileges and inherited environment of the VT Code process when the victim starts or resumes a session in that workspace. The command runs from the workspace and can read or modify files available to the current user, access any permitted local resources, and launch further processes. The impact is local command execution, not privilege escalation.Preconditions and Trust Boundary
The victim must run VT Code against the workspace and reach normal Agent session setup. The workspace configuration must contain a non-empty
session_starthook command; lifecycle hooks are empty by default. A user who explicitly creates a hook has intentionally enabled this product feature. The boundary issue is that later repository-controlled configuration content can select or change the command without a new, understandable approval bound to the final command and referenced files.Reproduction
Use a fresh temporary directory and build VT Code from
main@8fef6b4d(the tested build reports version0.143.4). Create a file namedvtcode.tomlwith the following content:Start a normal session from that workspace:
After the session UI initializes, interrupt the session if no local provider is available, then inspect the marker:
For the control group, use a second empty workspace without
vtcode.tomland run the same binary and session startup, adding--provider ollamaif needed to bypass first-run provider selection:The marker command is intentionally limited to writing a fixed file inside the PoC workspace. It does not read credentials, contact a remote endpoint, alter shell startup files, or delete data.
Observed Result
On
0.143.4built from8fef6b4d24fa42088203c999136000cbdfe62bb7, the configured workspace created.vtcode-session-start-markerwith the expected content during session setup. The no-configuration control did not create the marker. Both runs used fresh temporary home/config directories with noVTCODE_TRUST_WORKSPACEoverride or pre-existing trust record.The test harness did not answer VT Code's terminal cursor-position query, so interrupting the TUI reported a cleanup error after startup. The marker was created before that error, and the same terminal limitation affected the control group.
Source Evidence
src/startup/config_loading.rs:20-66resolves the workspace and builds the configuration;crates/codegen/vtcode-config/src/loader/manager.rs:128-168adds the workspace-rootvtcode.tomllayer and:248-265merges and validates the effective configuration.crates/codegen/vtcode-config/src/hooks.rs:18-32,132-145definessession_startand accepts a command string with validation limited to type, non-empty content, matcher syntax, and timeout bounds.src/agent/runloop/unified/session_setup/ui.rs:104-110constructsLifecycleHookEnginefrom the effective hooks;:380-394invokesrun_session_start()during session UI setup.crates/codegen/vtcode-core/src/hooks/lifecycle/engine.rs:69-98dispatches each matching command;:518-546executessh -c <configured command>in the workspace and callsspawn().src/agent/runloop/unified/session_setup/init.rs:183-190,691-698applies workspace trust toToolRegistryprompt policy, while the lifecycle engine is constructed and executed through the separate path above.Recommended Remediation
Before running a workspace-controlled lifecycle command, require an explicit per-workspace/per-launcher approval that displays the resolved shell command, working directory, and relevant environment policy. Bind the approval to the canonical workspace, repository revision or effective configuration digest, and the identity/content digest of referenced scripts. Revalidate those values immediately before every lifecycle spawn and after configuration reload. Do not reuse a tool-policy or full-auto trust bit as blanket approval for executable workspace configuration. Consider disabling workspace executable hooks by default or requiring a user-created local trust receipt before enabling them.
Acknowledgement
Thank you to @glmgbj233 for the detailed report, the careful dynamic reproduction, and the remediation suggestions that shaped the fix. Workspace-controlled lifecycle hooks now require explicit per-workspace approval of the exact command set before any command can run, and approvals are revalidated against the current configuration digest before every lifecycle spawn.