# How to Extend LazyOwn with Custom Python Modules

> Extend LazyOwn with custom Python modules by placing do_<command> files in the modules directory. Dynamically add new CLI commands without restarting the shell.

- Repository: [Grisuno/lazyown](https://github.com/grisuno/lazyown)
- Tags: how-to-guide
- Published: 2026-03-02

---

**Place a Python file containing `do_<command>` methods in the `modules/` directory and load it via the `run_script` command to dynamically register new CLI commands without restarting the interactive shell.**

LazyOwn is an open-source penetration testing framework built around a `cmd2`-based interactive shell (`LazyOwnShell`). Learning how to extend LazyOwn with custom Python modules enables you to integrate proprietary tooling, automate reconnaissance workflows, and add domain-specific capabilities while keeping the core codebase untouched.

## How LazyOwn Discovers Custom Commands

The framework supports four extension mechanisms, but Python modules offer the greatest flexibility. According to the source in [`lazyown.py`](https://github.com/grisuno/lazyown/blob/main/lazyown.py) and [`modules/agent_runner.py`](https://github.com/grisuno/lazyown/blob/main/modules/agent_runner.py), commands are registered through:

- **Built-in aliases** — Hard-coded in `LazyOwnShell.aliases` (lines 14-32 of [`lazyown.py`](https://github.com/grisuno/lazyown/blob/main/lazyown.py))
- **Lua plugins** — `.lua` files scanned by `load_plugins()` (lines 61-71)
- **YAML addons** — `.yaml` files processed by `load_yaml_plugins()` (lines 91-119)
- **Custom Python scripts** — `.py` files parsed by `ASTToolExtractor` in `LazyOwnShellWrapper` (lines 38-58 of [`modules/agent_runner.py`](https://github.com/grisuno/lazyown/blob/main/modules/agent_runner.py))

When you extend LazyOwn with custom Python modules, the `ASTToolExtractor` class walks the abstract syntax tree (AST) of your script to identify methods prefixed with `do_`, then injects them as runnable commands in the current session.

## Step-by-Step Guide to Creating a Custom Module

### 1. Create the Python File

Create a new file in the `modules/` directory. The filename does not constrain the command name; only the method names matter.

```bash
touch modules/my_custom_tool.py

```

### 2. Implement `do_<command>` Methods

Define a class containing one or more `do_<command>` methods. Each method becomes a top-level shell command. In [`modules/agent_runner.py`](https://github.com/grisuno/lazyown/blob/main/modules/agent_runner.py), the `ASTToolExtractor` specifically searches for these signatures to build `CommandMetadata` objects.

```python

# modules/my_custom_tool.py

import subprocess
from cmd2 import Cmd

class MyCustomTool(Cmd):
    """Example module demonstrating custom ping functionality."""

    def do_ping(self, args):
        """ping <host> - Execute ICMP ping against target."""
        if not args:
            self.perror("Usage: ping <host>")
            return
        try:
            result = subprocess.run(
                ["ping", "-c", "4", args],
                capture_output=True,
                text=True,
                timeout=30
            )
            self.poutput(result.stdout or result.stderr)
        except Exception as e:
            self.perror(f"Error: {e}")

```

### 3. Load the Module Dynamically

From the interactive shell, use the built-in `run_script` command:

```shell
LazyOwn > run_script modules/my_custom_tool.py

```

The `LazyOwnShellWrapper` (lines 38-53 of [`modules/agent_runner.py`](https://github.com/grisuno/lazyown/blob/main/modules/agent_runner.py)) executes the following sequence:
1. Parses the file with `ASTToolExtractor` to discover `do_` methods
2. Instantiates the containing class
3. Registers the command in the current session context

### 4. Execute Your Custom Command

Once loaded, invoke the command directly by its method suffix:

```shell
LazyOwn > ping 8.8.8.8

```

## Technical Deep Dive: The Loading Mechanism

Understanding the internals ensures you write compatible modules. The critical components reside in [`modules/agent_runner.py`](https://github.com/grisuno/lazyown/blob/main/modules/agent_runner.py):

**`ASTToolExtractor` (lines 38-53)** walks the Python AST to identify class methods starting with `do_`. It extracts the method name, docstring, and class reference without executing the code, enabling safe introspection of untrusted scripts before runtime.

**`execute_command` (lines 91-98)** runs the discovered command in an isolated thread with a configurable timeout (`COMMAND_TIMEOUT`). This prevents hanging operations from freezing the interactive shell. The wrapper also maintains a registry in `self.executed_commands` to prevent infinite recursion when commands invoke themselves.

## Persisting Modules Across Sessions

By default, modules loaded via `run_script` are active only for the current session. To auto-load custom Python modules on startup, modify the `LazyOwnShell.__init__` method in [`lazyown.py`](https://github.com/grisuno/lazyown/blob/main/lazyown.py) (lines 342-354):

```python

# Inside LazyOwnShell.__init__

self.scripts = [
    "modules/my_custom_tool.py",
    # Add additional persistent modules here

]

```

Alternatively, create a startup script in `lazyscripts/startup.ls` containing `run_script` calls for each module you want pre-loaded without modifying core files.

## Complete Working Example

The following example creates a reusable "hello" command that greets users:

```bash

# Create the module

cat > modules/hello_world.py <<'EOF'
from cmd2 import Cmd

class HelloWorld(Cmd):
    """Simple greeting demonstration."""

    def do_hello(self, args):
        """hello [name] - Print personalized greeting."""
        name = args.strip() or "world"
        self.poutput(f"Hello, {name}!")
EOF

# Start LazyOwn

python3 lazyown.py

```

Then inside the shell:

```shell
LazyOwn > run_script modules/hello_world.py
LazyOwn > hello
Hello, world!
LazyOwn > hello Alice
Hello, Alice!

```

## Summary

- **Place custom modules in `modules/`** — Any `.py` file works; the location is conventional but not enforced by the loader.
- **Use `do_<command>` naming** — The `ASTToolExtractor` in [`modules/agent_runner.py`](https://github.com/grisuno/lazyown/blob/main/modules/agent_runner.py) discovers these methods automatically via AST parsing.
- **Load with `run_script`** — Execute `run_script modules/your_file.py` to register commands dynamically without restarting.
- **Leverage `cmd2.Cmd` features** — Inherit from `cmd2.Cmd` to access `self.poutput()`, `self.perror()`, and argument parsing utilities.
- **Persist via `self.scripts`** — Add module paths to the `scripts` list in `LazyOwnShell.__init__` (lines 342-354) for automatic loading on startup.

## Frequently Asked Questions

### Do I need to restart LazyOwn after adding a new Python module?

No. The `run_script` command loads modules dynamically using `ASTToolExtractor` to parse the file and register `do_` methods instantly. However, if you modify a module after loading it, you must re-run `run_script` to refresh the command definitions in the current session.

### Can custom modules use third-party Python libraries?

Yes. Custom modules execute within the same Python interpreter as LazyOwn. Install dependencies via `pip` in your environment (e.g., `pip install requests`), then import them normally in your module. The `LazyOwnShellWrapper` does not sandbox imports or restrict package access.

### How does LazyOwn handle long-running or hanging commands?

The `execute_command` method in [`modules/agent_runner.py`](https://github.com/grisuno/lazyown/blob/main/modules/agent_runner.py) (lines 91-98) wraps execution in a timed thread using `COMMAND_TIMEOUT`. If your command exceeds this limit, the thread terminates automatically, returning control to the shell without freezing the interactive session.

### What prevents infinite loops if a command calls itself?

The wrapper tracks exact command-argument pairs in `self.executed_commands` (defined at lines 2-6 of [`modules/agent_runner.py`](https://github.com/grisuno/lazyown/blob/main/modules/agent_runner.py)). Before executing any command, it checks this registry and refuses duplicate invocations, preventing recursive loops and circular dependencies.

### Is there a way to auto-register all Python files in the `modules/` directory?

While no automatic directory scanner exists by default, you can achieve this by appending filenames to `self.scripts` in `LazyOwnShell.__init__` (lines 342-354) or by creating a startup script in `lazyscripts/startup.ls` that iterates through the directory with multiple `run_script` calls.