Skip to main content
Build Your Own Claude Cowork: Open-Source Local Agent with Shell Access

Build Your Own Claude Cowork: Open-Source Local Agent with Shell Access

·1506 words·8 mins
Alejandro AO
Author
Alejandro AO

Software Engineer and Educator. Developer Advocate at Hugging Face 🤗

I help you build AI Apps that just work.

Code for this project on GitHub.

Claude Cowork lets you grant folder access to an AI and have it execute tasks autonomously. It’s a powerful concept: instead of copying files and explaining context, you just say “organize these files” and the agent figures it out.

In this tutorial, we’ll build an open-source version using the OpenAI Agents SDK. Our agent will have shell access, web search, and URL fetching—all running locally with Ollama or through Hugging Face’s inference providers.

By the end, you’ll have a working local agent that can:

  • Execute shell commands in a sandboxed directory
  • Search the web and fetch URLs
  • Handle complex file operations autonomously
  • Work with any OpenAI-compatible API

Let’s build it.

Architecture
#

Here’s what we’re building:

┌─────────────────────────────────────┐
│         Your Task                   │
└───────────────┬─────────────────────┘
                ▼
┌─────────────────────────────────────┐
│      OpenAI Agents SDK              │
│   (Agent loop, tool dispatch)       │
└───────────────┬─────────────────────┘
                ▼
┌─────────────────────────────────────┐
│    Any OpenAI-Compatible API        │
│   Ollama (local) or HuggingFace     │
└───────────────┬─────────────────────┘
                ▼
┌─────────────────────────────────────┐
│           3 Tools                   │
│   run_shell / fetch_url / search    │
└─────────────────────────────────────┘

The design philosophy: shell-first. Instead of creating separate tools for read, write, move, copy, delete, and list—we give the agent a single run_shell tool with an allowlist of commands. This is simpler and more powerful.

Setup
#

Create your project directory and install dependencies:

mkdir opencowork && cd opencowork
pip install openai-agents httpx beautifulsoup4 ddgs python-dotenv

Create a .env file for your configuration:

# For Ollama (local)
OPENCOWORK_MODEL=qwen2.5:7b
OPENCOWORK_BASE_URL=http://localhost:11434/v1
OPENCOWORK_API_KEY=ollama

# Or for HuggingFace (cloud)
# OPENCOWORK_MODEL=Qwen/Qwen2.5-72B-Instruct:novita
# OPENCOWORK_BASE_URL=https://router.huggingface.co/v1
# OPENCOWORK_API_KEY=hf_your_token

Building the Tools
#

The key to this agent is the tool design. Let’s start with tools.py:

Shell Tool with Allowlist
#

import re
import shlex
import subprocess
from pathlib import Path
from agents import function_tool

ALLOWED_DIRECTORIES: list[Path] = []

ALLOWED_COMMANDS = {
    # File operations
    "ls", "cat", "head", "tail", "cp", "mv", "rm", "mkdir", "rmdir", "touch",
    # Search
    "find", "grep", "rg",
    # Text processing
    "sort", "uniq", "wc", "cut", "sed", "awk",
    # Scripting
    "python", "python3", "bash", "sh",
    # Version control
    "git",
    # Utilities
    "echo", "date", "stat", "file", "du",
}

The allowlist is the security boundary. Only these commands can run. We also block dangerous patterns:

BLOCKED_PATTERNS = [
    r"\bsudo\b", r"\bsu\b",           # Privilege escalation
    r"\bcurl\b", r"\bwget\b",         # Network (use our tools instead)
    r"\bchmod\b", r"\bchown\b",       # Permission changes
    r"/etc/passwd", r"\.ssh/",        # Sensitive paths
    r"\.\./",                          # Path traversal
]

Now the actual tool:

@function_tool
def run_shell(command: str) -> str:
    """Execute a shell command within the sandboxed directory.

    Allowed commands: ls, cat, cp, mv, rm, mkdir, find, grep,
    python, bash, git, and more standard Unix tools.

    Args:
        command: Shell command to execute

    Returns:
        Command output (stdout + stderr)
    """
    if not ALLOWED_DIRECTORIES:
        return "Error: No directories have been granted access."

    # Validate against allowlist
    is_valid, error = validate_command(command)
    if not is_valid:
        return f"Blocked: {error}"

    result = subprocess.run(
        command,
        shell=True,
        capture_output=True,
        text=True,
        timeout=60,
        cwd=str(ALLOWED_DIRECTORIES[0]),
    )

    output = result.stdout
    if result.stderr:
        output += f"\n[stderr]: {result.stderr}"

    return output if output.strip() else "Command completed (no output)"

The validation extracts base commands from pipes and chains:

def extract_base_commands(command: str) -> list[str]:
    """Extract base commands from a shell string."""
    parts = re.split(r'\s*(?:\||&&|\|\||;)\s*', command)

    commands = []
    for part in parts:
        part = part.strip()
        if not part:
            continue
        try:
            tokens = shlex.split(part)
            if tokens:
                for token in tokens:
                    if '=' not in token or token.startswith('-'):
                        commands.append(token)
                        break
        except ValueError:
            words = part.split()
            if words:
                commands.append(words[0])

    return commands


def validate_command(command: str) -> tuple[bool, str]:
    """Validate command against allowlist and blocklist."""
    # Check blocked patterns
    for pattern in BLOCKED_PATTERNS:
        if re.search(pattern, command, re.IGNORECASE):
            return False, f"Blocked pattern: {pattern}"

    # Validate all base commands
    for cmd in extract_base_commands(command):
        cmd_name = Path(cmd).name
        if cmd_name not in ALLOWED_COMMANDS:
            return False, f"Not in allowlist: {cmd_name}"

    return True, ""

This handles commands like find . -name "*.pdf" | head -10 by checking both find and head against the allowlist.

Web Tools
#

For research tasks, we add URL fetching and web search:

import httpx
from bs4 import BeautifulSoup
from ddgs import DDGS

@function_tool
def fetch_url(url: str) -> str:
    """Fetch and extract text content from a URL.

    Args:
        url: URL to fetch (must start with http:// or https://)

    Returns:
        Extracted text content from the page
    """
    response = httpx.get(url, timeout=15, follow_redirects=True)
    soup = BeautifulSoup(response.text, "html.parser")

    # Remove non-content elements
    for tag in soup(["script", "style", "nav", "footer"]):
        tag.decompose()

    text = soup.get_text(separator="\n", strip=True)
    return text[:8000] if len(text) > 8000 else text


@function_tool
def search_web(query: str) -> str:
    """Search the web using DuckDuckGo.

    Args:
        query: Search query

    Returns:
        Formatted search results
    """
    results = list(DDGS().text(query, max_results=5))

    formatted = []
    for i, r in enumerate(results, 1):
        formatted.append(
            f"{i}. **{r['title']}**\n"
            f"   {r['href']}\n"
            f"   {r.get('body', '')[:200]}"
        )

    return "\n\n".join(formatted)

Creating the Agent
#

Now we wire up the agent in agent.py:

import os
from agents import Agent, OpenAIChatCompletionsModel
from openai import AsyncOpenAI
from tools import run_shell, fetch_url, search_web

MODEL_ID = os.environ.get("OPENCOWORK_MODEL", "qwen2.5:7b")
BASE_URL = os.environ.get("OPENCOWORK_BASE_URL", "http://localhost:11434/v1")
API_KEY = os.environ.get("OPENCOWORK_API_KEY", "ollama")

client = AsyncOpenAI(base_url=BASE_URL, api_key=API_KEY)
model = OpenAIChatCompletionsModel(model=MODEL_ID, openai_client=client)

agent = Agent(
    name="OpenCowork",
    instructions="""You are OpenCowork, a local task assistant with shell access.

# Your Tools

1. **run_shell** - Execute shell commands in the granted directory
2. **fetch_url** - Fetch and extract text from web pages
3. **search_web** - Search the web for information

# How to Work

Use shell efficiently:
- Prefer one-liners over multiple commands
- Use pipes and chaining (|, &&)
- For repetitive tasks, write loops or scripts

Examples:
```bash
ls -la                           # List files
mkdir -p images && mv *.jpg images/  # Organize
find . -name "*.pdf" | head -10  # Search
grep -r "pattern" --include="*.txt"  # Content search

Rules
#

  • All operations are sandboxed to the granted directory
  • Be careful with rm - ask if unsure
  • If something fails, try an alternative approach “”", tools=[run_shell, fetch_url, search_web], model=model, )

The instructions teach the agent to use shell efficiently. Good prompts here make a huge difference in agent behavior.

## The Main Loop

Finally, `main.py` ties everything together:

```python
import asyncio
from pathlib import Path
from agents import Runner
import tools
from agent import agent
from dotenv import load_dotenv

load_dotenv()


def grant_folder_access():
    """Prompt user to grant folder access."""
    while True:
        folder = input("\n📁 Grant folder access (path): ").strip()
        path = Path(folder).expanduser().resolve()

        if not path.exists() or not path.is_dir():
            print(f"❌ Invalid path: {path}")
            continue

        tools.ALLOWED_DIRECTORIES.append(path)
        print(f"✅ Access granted to: {path}")
        return path


async def run_task(task: str) -> str:
    """Run a task through the agent."""
    result = await Runner.run(agent, task, max_turns=30)
    return result.final_output


async def main():
    print("OPENCOWORK - Local Task Assistant")
    working_dir = grant_folder_access()
    print(f"\n🚀 Ready. Working directory: {working_dir}")

    while True:
        task = input("📋 Task: ").strip()

        if task.lower() == "quit":
            break
        if task.lower() == "grant":
            grant_folder_access()
            continue

        print("\n⏳ Working...\n")
        result = await run_task(task)
        print(f"\n📝 Result:\n{result}\n")


if __name__ == "__main__":
    asyncio.run(main())

Running It
#

Start the agent:

python main.py

Grant access to a folder, then try some tasks:

📋 Task: Organize this folder by file type
📋 Task: Find all images larger than 1MB
📋 Task: Create a script to find duplicate files
📋 Task: Research "topic" and save a summary

The agent will use shell commands to accomplish these tasks, chaining tools as needed.

Using Different Providers
#

Ollama (Local)
#

Run models locally for privacy:

OPENCOWORK_MODEL=qwen2.5:7b
OPENCOWORK_BASE_URL=http://localhost:11434/v1
OPENCOWORK_API_KEY=ollama

HuggingFace (Cloud)
#

Use the inference router for access to many models:

OPENCOWORK_MODEL=Qwen/Qwen2.5-72B-Instruct:novita
OPENCOWORK_BASE_URL=https://router.huggingface.co/v1
OPENCOWORK_API_KEY=hf_your_token

The model:provider format lets you choose providers like :novita, :together, :fireworks-ai, or :fastest for automatic routing.

Security Considerations
#

This is a demo. The security model relies on:

  1. Allowlist: Only specific commands can run
  2. Path blocking: ../ traversal and sensitive paths blocked
  3. Directory sandboxing: Commands run in the granted directory

For production, add OS-level sandboxing (Docker, bubblewrap, etc.). Never run this on sensitive systems without proper isolation.

Extending the Agent
#

Want more capabilities? Add tools:

@function_tool
def read_clipboard() -> str:
    """Read the current clipboard contents."""
    import subprocess
    return subprocess.run(["pbpaste"], capture_output=True, text=True).stdout


@function_tool
def take_screenshot(filename: str) -> str:
    """Take a screenshot and save it."""
    import subprocess
    subprocess.run(["screencapture", "-x", filename])
    return f"Screenshot saved to {filename}"

Or expand the allowlist for specific use cases.

Recap
#

We built an open-source Claude Cowork alternative with:

  • Shell-first design: One tool replaces many file operations
  • Allowlist security: Only permitted commands can run
  • Provider flexibility: Works with Ollama, HuggingFace, or any OpenAI-compatible API
  • Minimal codebase: ~300 lines of Python

The key insight is that giving an agent shell access with an allowlist is more flexible than creating dozens of specific tools. The agent can compose commands, use pipes, and write scripts—all within security boundaries.

Full Code
#

The complete project is available on GitHub: alejandro-ao/opencowork

# agent.py
import os
from agents import Agent, OpenAIChatCompletionsModel
from openai import AsyncOpenAI
from tools import run_shell, fetch_url, search_web

MODEL_ID = os.environ.get("OPENCOWORK_MODEL", "qwen2.5:7b")
BASE_URL = os.environ.get("OPENCOWORK_BASE_URL", "http://localhost:11434/v1")
API_KEY = os.environ.get("OPENCOWORK_API_KEY", "ollama")

client = AsyncOpenAI(base_url=BASE_URL, api_key=API_KEY)
model = OpenAIChatCompletionsModel(model=MODEL_ID, openai_client=client)

agent = Agent(
    name="OpenCowork",
    instructions="""You are OpenCowork, a local task assistant with shell access.
Your tools: run_shell, fetch_url, search_web.
Use shell efficiently with pipes and chaining.
All operations are sandboxed to the granted directory.""",
    tools=[run_shell, fetch_url, search_web],
    model=model,
)

References
#

Buy Me A Coffee
undefined