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 oflazyown.py) - Lua plugins —
.luafiles scanned byload_plugins()(lines 61-71) - YAML addons —
.yamlfiles processed byload_yaml_plugins()(lines 91-119) - Custom Python scripts —
.pyfiles parsed byASTToolExtractorinLazyOwnShellWrapper(lines 38-58 ofmodules/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:
- Parses the file with
ASTToolExtractorto discoverdo_methods - Instantiates the containing class
- 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.pyfile works; the location is conventional but not enforced by the loader. - Use
do_<command>naming — TheASTToolExtractorinmodules/agent_runner.pydiscovers these methods automatically via AST parsing. - Load with
run_script— Executerun_script modules/your_file.pyto register commands dynamically without restarting. - Leverage
cmd2.Cmdfeatures — Inherit fromcmd2.Cmdto accessself.poutput(),self.perror(), and argument parsing utilities. - Persist via
self.scripts— Add module paths to thescriptslist inLazyOwnShell.__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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →