How to Extend LazyOwn with Custom Python Modules

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 and modules/agent_runner.py, commands are registered through:

  • Built-in aliases — Hard-coded in LazyOwnShell.aliases (lines 14-32 of 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)

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.

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, the ASTToolExtractor specifically searches for these signatures to build CommandMetadata objects.


# 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:

LazyOwn > run_script modules/my_custom_tool.py

The LazyOwnShellWrapper (lines 38-53 of 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:

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:

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 (lines 342-354):


# 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:


# 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:

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 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 (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). 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →