Abstract / Overview

This article presents a detailed walkthrough of how to build a natural-language Bash terminal agent using the NVIDIA Nemotron Nano 9B v2 model. The system allows you to issue high-level instructions in plain English, have the agent interpret intent, choose safe Bash commands, ask for confirmation, execute them, and return results. It stems from the official NVIDIA blog “Create Your Own Bash Computer Use Agent with NVIDIA Nemotron in One Hour” (Oct 22, 2025) by Mehran Maghoumi. (NVIDIA Developer)

Conceptual Background

bash_agent_nemotron_hero

What is a “computer‐use agent”?

A computer‐use agent is a tool that receives high-level instructions in natural language and internally converts them into actions (e.g., Bash commands) on the computer, then returns results. Unlike a chatbot that only replies with text, a true agent acts. The NVIDIA blog emphasises: “You provide a high-level instruction … it decides which Bash commands to run via tool calling.” (NVIDIA Developer)

Key enabling technology: tool calling/function calling via an LLM. The model reasons about intent, picks commands, executes via wrapper, receives stdout/stderr, and adapts next step. (NVIDIA Developer)

Why use NVIDIA Nemotron Nano 9B v2?

Note: If you lack that hardware, you can use cloud endpoints (e.g., via OpenRouter) as shown. (NVIDIA Developer)

Four core agent considerations

The blog summarises key concerns when building such agents:

Step-by-Step Walkthrough

Prerequisites

Architecture Overview

The system consists of:

  1. Bash tool/wrapper: a Python class that wraps subprocess.run, keeps track of the current working directory, ensures only allowed commands, and prompts for user confirmation. (NVIDIA Developer)

  2. Agent: The LLM (Nemotron) plus system prompt defines role, allowed commands, behaviour. The loop: user → model → maybe tool call → user confirmation → execution → result → feedback → next iteration. See diagram:

bash_agent_nemotron_workflow

Writing the Bash Class

Here is a simplified version of the Bash tool class (from the blog).

from typing import List, Dict, Any
import subprocess
import os

class Bash:
    def __init__(self, cwd: str, allowed_commands: List[str]):
        self.cwd = cwd
        self._allowed_commands = allowed_commands

    def _extract_commands(self, cmd: str) -> List[str]:
        # Very simple parse for the first word of each pipe/semicolon segment
        return [part.strip().split()[0] for part in cmd.split(';') if part.strip()]

    def exec_bash_command(self, cmd: str) -> Dict[str, str]:
        if not cmd:
            return {"error": "No command was provided"}
        for cmd_part in self._extract_commands(cmd):
            if cmd_part not in self._allowed_commands:
                return {"error": f"Parts of this command were not in the allowlist: {cmd_part}"}
        return self._run_bash_command(cmd)

    def _run_bash_command(self, cmd: str) -> Dict[str, str]:
        stdout = ""
        stderr = ""
        new_cwd = self.cwd
        try:
            wrapped = f"{cmd}; echo __END__; pwd"
            result = subprocess.run(
                wrapped, shell=True, cwd=self.cwd,
                capture_output=True, text=True, executable="/bin/bash"
            )
            stderr = result.stderr
            splits = result.stdout.split("__END__")
            stdout = splits[0].strip()
            if not stdout and not stderr:
                stdout = "Command executed successfully, with no output."
            new_cwd = splits[-1].strip()
            self.cwd = new_cwd
        except Exception as e:
            stdout = ""
            stderr = str(e)
        return {"stdout": stdout, "stderr": stderr, "cwd": new_cwd}

    def to_json_schema(self) -> Dict[str, Any]:
        return {
            "type": "function",
            "function": {
                "name": "exec_bash_command",
                "description": "Execute a bash command and return stdout/stderr and the working directory",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "cmd": {
                            "type": "string",
                            "description": "The bash command to execute"
                        }
                    },
                    "required": ["cmd"]
                }
            }
        }

Explanation:

Writing the Agent Loop From Scratch

Define the system prompt (with allowed commands list) and implement a loop:

SYSTEM_PROMPT = f"""/think
You are a helpful Bash assistant with the ability to execute commands in the shell.
You engage with users to help answer questions about bash commands, or execute their intent.
If user intent is unclear, keep engaging with them to figure out what they need and how to best help them.
If they ask question that are not relevant to bash or computer use, decline to answer.
When a command is executed, you will be given the output from that command and any errors. Based on that, either take further actions or yield control to the user.
You are only allowed to execute the following commands:
{LIST_OF_ALLOWED_COMMANDS}
**Never** attempt to execute a command not in this list. **Never** attempt to execute dangerous commands like `rm`, `mv`, `rmdir`, `sudo`, etc. If the user asks you to do so, politely refuse.
When you switch to new directories, always list files so you can get more context.
"""

Then the loop:

bash = Bash(cwd=os.getcwd(), allowed_commands=MY_ALLOWLIST)
llm = LLM(...)  # client connecting to Nemotron endpoint
messages = Messages(SYSTEM_PROMPT)
while True:
    user = input("['🙂] ").strip()
    messages.add_user_message(user)
    while True:
        response, tool_calls = llm.query(messages, [bash.to_json_schema()])
        messages.add_assistant_message(response)
        if tool_calls:
            for tc in tool_calls:
                fn = tc.function.name
                args = json.loads(tc.function.arguments)
                if fn == "exec_bash_command" and "cmd" in args:
                    if confirm_execution(args["cmd"]):
                        result = bash.exec_bash_command(args["cmd"])
                    else:
                        result = {"error": "The user declined the execution of this command."}
                else:
                    result = {"error": "Incorrect tool or function argument"}
                messages.add_tool_message(result, tc.id)
        else:
            print(f"\n[🤖] {response.strip()}")
            break

This loop allows the agent to make multiple tool calls if required for a single user request. (NVIDIA Developer)

Bonus: Simplifying With LangGraph

If you install LangGraph, you can replace much of the manual loop code with:

from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.memory import InMemorySaver
from langchain_openai import ChatOpenAI

class ExecOnConfirm:
    def __init__(self, bash: Bash):
        self.bash = bash
    def _confirm_execution(self, cmd: str) -> bool:
        return input(f"    ▶️   Execute '{cmd}'? [y/N]: ").strip().lower() == "y"
    def exec_bash_command(self, cmd: str) -> Dict[str, str]:
        if self._confirm_execution(cmd):
            return self.bash.exec_bash_command(cmd)
        return {"error": "The user declined the execution of this command."}

bash = Bash(...)
agent = create_react_agent(
    model=ChatOpenAI(model=...),
    tools=[ExecOnConfirm(bash).exec_bash_command],
    prompt=SYSTEM_PROMPT,
    checkpointer=InMemorySaver(),
)

while True:
    user = input("[🙂] ").strip()
    result = agent.invoke({"messages": [{"role":"user","content": user}]})
    response = result["messages"][-1].content.strip()
    if "</think>" in response:
        response = response.split("</think>")[-1].strip()
    if response:
        print(f"\n[🤖] {response}")

This reduces boilerplate and error handling overhead. (NVIDIA Developer)

Use-Cases / Scenarios

Limitations / Considerations

Fixes (Common Pitfalls & Troubleshooting)

FAQs

Q1: Can I run this agent without a GPU locally?
A1: You could, but running the full Nemotron Nano 9B v2 model efficiently requires significant GPU resources (~24 GB VRAM) per the blog. Cloud endpoints are suggested. (NVIDIA Developer)

Q2: How do I choose the allowlist of commands?
A2: Start with minimal safe commands: ls, pwd, cat, grep, touch, mkdir, df, free, echo. Exclude destructive commands (rm, mv, rmdir, sudo). The blog emphasises rejecting anything outside the list. (NVIDIA Developer)

Q3: What happens if the agent fails a command?
A3: The wrapper returns stderr and the new cwd (or error message). The LLM receives this and can reason about the next step (retry, fix syntax, ask user). The system prompt instructs it to handle errors. (NVIDIA Developer)

Q4: Can I extend this to multiple tools beyond Bash?
A4: Yes. The same pattern applies: define a tool wrapping class, a schema for tool-calling, and update the prompt accordingly. You could integrate database queries, web APIs, cloud commands, etc. The blog hints at multi-agent systems built with Nemotron. (NVIDIA Developer)

Q5: Is the code open source?
A5: Yes: The blog links to the agent code on GitHub. (NVIDIA Developer)

References

Conclusion

You can build a working natural-language Bash agent in approximately an hour, using the NVIDIA Nemotron Nano 9B v2 model along with a lightweight Python wrapper for Bash. The tutorial covers the core components: tool wrapper, allowed commands, state tracking, LLM prompt, and loop logic (from scratch or via LangGraph). While the demo is simple, the architecture scales, allowing you to extend it to other tools, build complex workflows, and integrate it into production systems. Pay attention to safety (command allowlist, human confirmation), resource constraints (GPU/endpoint), and system robustness (error handling, state management).