AI coding agents can do more than generate code. Depending on the tools available to them, they can execute terminal commands, install packages, modify files, run tests, start services, and interact with development environments.
These capabilities make agents useful for software development, but they also introduce security risks. An agent asked to fix a compilation error might attempt to delete a directory, modify Git history, install an untrusted package, or execute a command that exposes sensitive environment variables.
The problem is not limited to malicious instructions. An agent can misunderstand a task, follow unsafe instructions found in a repository, or generate a command that has unintended consequences.
The safest approach is to treat every terminal operation initiated by an AI agent as an untrusted request. The application should validate the requested operation, enforce permissions, and execute approved commands through a controlled mechanism.
This article explains how to build that protection into a C# application.
Why AI Agent Terminal Access Is a Security Risk
A terminal provides access to many operations that can affect the entire development environment. The risk depends on the operating system, the current user's permissions, the working directory, and the tools installed on the machine.
For example, an agent might request the following commands:
dotnet test
git status
git reset --hard
The first two commands are commonly used during development. The third can discard uncommitted changes. Whether a command is safe depends on its arguments, execution context, and effects.
A simple rule that blocks one dangerous command name is not enough. An agent could invoke another executable, combine commands through a shell, or use an otherwise permitted program to modify files.
Common risks include:
- Destructive file operations that delete source code or configuration.
- Git commands that discard changes or rewrite repository history.
- Package installation that introduces malicious or vulnerable dependencies.
- Network commands that send source code, credentials, or other sensitive data outside the environment.
- Privilege escalation attempts or access to files outside the intended project.
- Commands that consume excessive CPU, memory, disk space, or execution time.
The goal is not to prevent agents from using the terminal entirely. It is to give them only the operations needed to complete their assigned tasks.
Use an Allowlist Instead of Trusting Generated Commands
An allowlist defines the operations an agent is permitted to request. Everything outside that list is rejected unless an explicit authorization process permits it.
For example, a development agent might be allowed to inspect Git status, review a diff, build a solution, and run approved tests.
| Operation | Default policy | Reason |
|---|
| Read Git status | Allow | Inspects repository state |
| Review a Git diff | Allow with limits | Reads pending changes |
| Build a known solution | Allow with limits | Runs project build targets |
| Run approved tests | Allow with limits | Executes test code |
| Install a new package | Require approval | Downloads and executes external build logic |
| Delete project files | Deny or require explicit approval | Can destroy source or configuration |
| Rewrite Git history | Deny by default | Can discard or replace repository history |
| Execute arbitrary shell commands | Deny | Provides a broad execution capability |
An allowlist should identify approved operations, not simply trusted executable names.
For example, permitting dotnet does not automatically make every dotnet command safe. Package management, custom tools, build targets, and project scripts can execute code. Similarly, permitting git does not make every Git subcommand or argument harmless.
A better design exposes named operations such as BuildSolution and ReadGitStatus rather than allowing the agent to submit an unrestricted command string.
Build a Restricted Command Runner in C#
The following example demonstrates a small command broker. The agent requests a predefined operation, and the application determines which executable and arguments to use.
This approach avoids passing arbitrary agent-generated strings to a shell.
Define the Allowed Operations
public enum AgentOperation
{
ReadGitStatus,
BuildSolution,
RunTests
}
The agent can request only one of these operations. It cannot directly supply a new executable path or append arbitrary shell commands.
The enum is not a complete security boundary by itself. The server-side code must enforce the policy, and the model must never be allowed to bypass the broker by accessing another terminal tool.
Implement the Command Broker
using System.Diagnostics;
public sealed class AgentCommandBroker
{
private readonly string _projectDirectory;
public AgentCommandBroker(string projectDirectory)
{
_projectDirectory = Path.GetFullPath(projectDirectory);
}
public async Task<string> ExecuteAsync(
AgentOperation operation,
CancellationToken cancellationToken)
{
var startInfo = new ProcessStartInfo
{
FileName = "dotnet",
WorkingDirectory = _projectDirectory,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
switch (operation)
{
case AgentOperation.ReadGitStatus:
startInfo.FileName = "git";
startInfo.ArgumentList.Add("status");
startInfo.ArgumentList.Add("--short");
break;
case AgentOperation.BuildSolution:
startInfo.ArgumentList.Add("build");
startInfo.ArgumentList.Add("MyApplication.sln");
startInfo.ArgumentList.Add("--no-restore");
break;
case AgentOperation.RunTests:
startInfo.ArgumentList.Add("test");
startInfo.ArgumentList.Add("MyApplication.Tests.csproj");
startInfo.ArgumentList.Add("--no-restore");
break;
default:
throw new InvalidOperationException(
"The requested operation is not permitted.");
}
using var process = new Process
{
StartInfo = startInfo
};
process.Start();
Task<string> outputTask =
process.StandardOutput.ReadToEndAsync(
cancellationToken);
Task<string> errorTask =
process.StandardError.ReadToEndAsync(
cancellationToken);
await process.WaitForExitAsync(cancellationToken);
string output = await outputTask;
string error = await errorTask;
if (process.ExitCode != 0)
{
throw new InvalidOperationException(
$"Command failed with exit code {process.ExitCode}: " +
error);
}
return output;
}
}
Replace the example solution and test project names with the actual paths used by your application.
Several details are important here.
First, the switch statement maps each permitted operation to predefined arguments. The agent cannot replace the test project path or inject additional command-line arguments through the operation request.
Second, UseShellExecute is set to false. The application starts the executable directly instead of passing the command through a shell such as cmd.exe or Bash. This reduces shell injection risk, although it does not make the executable or its arguments inherently safe.
Third, ArgumentList passes arguments individually rather than constructing one shell command string. Avoid replacing it with code that concatenates agent-generated text into a command.
Finally, the runner checks the exit code and collects output so the application can report failures.
This example is a starting point, not a complete production sandbox. It needs additional controls for execution time, output size, executable paths, environment variables, process-tree termination, and access to the filesystem. A cancellation token alone does not impose a time limit unless the caller supplies one.
Add Timeouts and Resource Limits
Even a permitted command can become dangerous if it runs indefinitely or consumes excessive resources.
For example, a test command might wait for an unavailable service, while a build process might generate large amounts of output.
The application should apply an execution deadline and terminate the process when the deadline expires. A production implementation should also terminate the process tree, because child processes can outlive the original command.
A basic timeout pattern looks like this:
using var timeoutCts =
new CancellationTokenSource(TimeSpan.FromMinutes(2));
using var linkedCts =
CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken,
timeoutCts.Token);
await process.WaitForExitAsync(linkedCts.Token);
This snippet shows how to apply a timeout to the wait operation. It does not, by itself, terminate the process. If cancellation occurs, the surrounding execution code must explicitly kill the process tree, handle cleanup, and stop reading redirected output safely.
For production systems, enforce additional limits outside the process as well:
- CPU and memory limits.
- Maximum output size.
- Disk and temporary-directory quotas.
- Maximum concurrent commands.
- Network restrictions.
- A bounded execution duration.
Operating-system containers or dedicated worker processes can enforce these restrictions more reliably than application-level checks alone.
Validate the Working Directory and File Access
Restricting commands does not automatically restrict the files those commands can access.
A build command may run scripts defined in a project. A test may access environment variables or write outside the project directory. A tool can also follow symbolic links to reach locations that appear to be inside the workspace.
Start by resolving the project directory to an absolute path. Do not trust a working directory supplied by the agent.
For file operations, validate the resolved target path against the permitted root, including appropriate handling of symbolic links and platform-specific path rules.
However, path validation in application code is not sufficient for a hostile environment. A process may access files directly without using the application's file APIs.
Use operating-system permissions, an isolated workspace, and a restricted service account to enforce the actual filesystem boundary. Keep credentials, production configuration, and unrelated repositories outside the agent's accessible environment.
Separate Approval From Execution
Some operations are too risky for automatic approval, even when they are legitimate development tasks.
Examples include installing dependencies, modifying database schemas, deleting files, pushing changes to a remote repository, or deploying an application.
A command approval workflow should show the requested operation, its arguments, the affected environment, and the reason for the request.
The application should then require approval from an authorized person or policy engine before execution.
Approval must be enforced by the trusted command broker, not merely requested through the AI conversation. The agent should not be able to mark its own operation as approved.
For higher-risk operations, bind approval to the exact operation and arguments. If the command changes after approval, require a new decision.
Log Every Command for Security Audits
A restricted command runner should produce an audit record for each request, including denied operations.
Useful fields include:
| Field | Purpose |
|---|
| Workflow ID | Connects the command to the AI task |
| Agent ID | Identifies the requesting agent |
| Operation | Records the requested action |
| Approval decision | Explains whether the action was permitted |
| Start and end times | Measures execution duration |
| Exit code | Records the command result |
| Working environment | Identifies the workspace or isolated worker |
| Correlation ID | Connects related application logs |
Do not store secrets or unrestricted terminal output in audit logs. Output can contain credentials, connection strings, or sensitive source code. Apply appropriate access controls and retention policies.
For high-risk operations, store audit records in a location that the agent cannot modify or delete.
Common Mistakes to Avoid
Blocking Only Dangerous Command Names
A denylist that blocks commands such as rm or del can be bypassed by other executables, scripts, or indirect file operations. Use an allowlist of specific operations and enforce isolation.
Passing Agent Output Directly to a Shell
Constructing a shell command from model-generated text creates injection risks. Use predefined operations and argument lists, and avoid shell execution unless there is a clearly justified requirement.
Assuming a Container Makes Everything Safe
Containers provide useful isolation, but their security depends on configuration. Excessive privileges, mounted host directories, exposed credentials, and unrestricted network access can undermine the boundary.
Allowing Unrestricted Build and Test Scripts
Build systems and test suites can execute arbitrary code. Run them with limited privileges and access to only the required resources.
Ignoring Background Processes
Terminating a parent process does not always stop every child process. Manage process trees and apply operating-system-level resource controls.
Best Practices for Production Applications
- Start with read-only operations and grant write access only when needed.
- Expose named operations rather than arbitrary command execution.
- Use fixed executable paths and validated arguments.
- Run agents under a dedicated, low-privilege account.
- Keep secrets outside the agent's accessible environment.
- Require explicit authorization for destructive or externally visible actions.
- Enforce CPU, memory, time, output, and network limits.
- Log denied attempts as well as successful executions.
- Test the policy using malicious arguments, path traversal attempts, shell metacharacters, and unexpected tool requests.
- Revalidate permissions whenever an agent or workflow receives additional capabilities.
Advantages and Disadvantages
A restricted command broker reduces the number of actions an AI agent can perform and provides a central place to enforce policy. It also improves auditability and makes security testing easier.
The trade-off is additional engineering work. Strict allowlists may block legitimate workflows, and each newly supported operation needs careful review. Sandboxing, monitoring, and approval processes also introduce operational overhead.
These costs are generally easier to manage than giving an autonomous agent unrestricted access to a developer workstation or production environment.
Summary
AI coding agents should not receive unrestricted terminal access simply because they need to build and test software. Every command should pass through a trusted policy layer that validates the requested operation and applies the correct permissions.
In C#, a command broker with predefined operations, ProcessStartInfo, and ArgumentList provides a useful starting point. For production use, combine it with execution timeouts, process-tree management, filesystem isolation, least-privilege permissions, approval workflows, and durable audit logging.
The central security principle is straightforward: the AI agent can request an operation, but the application must decide whether that operation is allowed and enforce the decision outside the model itself.
Join the conversation! Your thoughts help the community grow.