Guides
Authoring Modules
Design and build your own redteamer modules. This guide walks through setting up a git repo, wiring up an LSP-powered editor, and authoring your first *.rt.hcl module with inputs, actions, and outputs.
Overview
redteamer modules are HCL files (*.rt.hcl) stored in git repos.
This document focuses on authoring modules. Not on building new tools from scratch. You’ll typically wrap tools you already use (nmap, gobuster, custom scripts, etc.) in a consistent, auditable interface, and then compose those modules into higher-level workflows.
How to think about modules
At a high level, a module is a small, declarative description of an operation. That operation might be:
- a single action performed by one tool, or
- a phase in a larger workflow composed of multiple modules.
It can be tempting to design modules as end-to-end attack chains that promise to scan the network and auto-exploit everything. In practice, these are brittle, error-prone, and never a substitute for operator judgment.
It may also be tempting to start wrapping tools 1:1 and re-exposing all CLI flags as inputs. These modules are a lot of work to maintain and offer little value over running the tool directly: they have too many knobs, weak defaults, and don’t encode any knowledge. Prefer opinionated modules with a small set of meaningful inputs that reflect how your team actually uses the tool for a particular task.
The most useful modules are boring but reliable building blocks:
- Per-tool modules wrap a single tool doing one well-defined job.
- Transformation modules normalize, filter, merge, or enrich data between steps.
- Workflow (phase-level) modules orchestrate several modules for a specific phase, such as internal discovery, external web surface triage, or an AD identity overview.
Modules can let senior operators capture institutional knowledge in executable form. Their tradecraft moves from docs and one-off scripts to reusable modules that juniors can run with consistent defaults, safer guardrails, and good observability while still being free to compose them into novel operations.
Create a Git Repo
Modules live in git so they can be:
- versioned and reviewed
- shared between operators
- pinned and audited
From a fresh directory:
git init evilcorp-utils
cd evilcorp-utils
A simple layout:
evilcorp-utils/
README.md
web/
dirbscan.rt.hcl
aws/
s3-enum.rt.hcl
Grouping by domain (web/, aws/, ad/) makes discovery easier.
If this is going to be shared:
git remote add origin git@github.com:evilcorp/utils.git
git add .
git commit -m "Initial module repo"
git push -u origin main
Later, operators can point redteamer at this repo:
rt prep https://github.com/evilcorp/utils
Use an LSP-Powered Editor
redteamer ships an LSP (Language Server Protocol) endpoint so you can get:
- syntax highlighting & completion for HCL
- inline validation and errors
- hover help for functions and attributes
In your editor:
- configure a new language server named e.g.
redteamer-lsp - point its command to
rt serve lsp - associate it with
*.rt.hclfiles
Once connected, you’ll get immediate feedback while editing modules—much nicer than discovering syntax errors at runtime.
Create Your First *.rt.hcl File
Inside your repo, create a file for your module, for example:
mkdir -p modules/web
$EDITOR modules/web/whoami.rt.hcl
A minimal starting point might look like:
module {
description = "Report the current user"
}
action "whoami" {
command = ["whoami"]
}
output "username" {
type = string
description = "Username reported by the process"
example = "root"
value = chomp(action.whoami.stdout)
}
This isn’t yet a useful module, but it shows the basic building blocks:
- an
actionblock that describes what to run - an
outputblock that describes an output data shape - a data transformation function that strips the trailing newline
As you type, your LSP-powered editor should start offering completions and diagnostics.
Define Inputs
Inputs are the knobs an operator (or CI, or an API client) can turn. Conceptually:
- each input has a name, type, optional default, and a description
- inputs become values you can reference via
input.<name> - they surface automatically as flags in
rt runand as fields in the REST / MCP tool interfaces
A simple input set:
input "target" {
type = string
description = "Target host or URL"
}
input "timeout" {
type = number
default = 30
description = "Timeout in seconds per attempt"
}
You might then reference them:
# Interpolated into strings
url = "http://${input.target}/"
# Used in expressions
effective_timeout = input.timeout > 0 ? input.timeout : 30
Good Input Hygiene:
- Prefer explicit types (
string,number,bool) over genericany. - Use sensible defaults where possible so
rt run <module>“just works”. - Validate values with functions where relevant, e.g.:
assert { condition = is(var.target, "ipv4") || is(var.target, "url") error_msg = "Must be valid IPv4 address or URL" } - Keep input names short but descriptive:
domain,wordlist,region, etc.
Define Actions
Actions are where the work happens. Each action block:
- can call tools, scripts, APIs, or even other modules
- produces structured results that you can pass into outputs or downstream modules
A simple action that calls a directory brute-forcer:
action "dir_scan" {
# Reuse an existing tool module
with = "core/tools/gobuster/dir"
# Optional conditional execution
when = input.enable_scanning
base_url = input.target
wordlist = input.wordlist
threads = 10
}
Because actions are regular HCL blocks:
- you can loop with
for_eachorcount - you can branch with
when - you can use functions (
tolist,flatten,timeadd,scan, etc.) to shape inputs/outputs
Example with for_each:
action "dir_scan" {
for_each = toset(input.targets)
with = "core/tools/gobuster/dir"
base_url = "http://${each.value}/"
wordlist = input.wordlist
}
Later, you can aggregate results with splats:
var {
all_results = values(action.dir_scan)[*].results
}
Define Outputs
Outputs are the published interface of a module run. They:
- show up in
rt explain <module>andrt output <run-id> - can be referenced by other modules
- are included in audit logs, API responses, and MCP tool results
A simple output:
output "found_paths" {
type = list(string)
description = "Discovered HTTP paths"
example = ["/admin", "/login"]
value = action.dir_scan.results.paths
}
You can also build richer objects:
output "summary" {
description = "Summary but this would be better as indivudual output blocks"
type = object({
target = string
total_paths = number
started_at = string
finished_at = string
elapsed_human = string
})
value = {
target = var.target
total_paths = len(action.dir_scan.results.paths)
started_at = action.dir_scan.started_at
finished_at = action.dir_scan.finished_at
elapsed_human = fmttime(action.dir_scan.started_at)
}
}
For multi-target actions:
output "all_results" {
value = flatten(values(action.dir_scan)[*].results.paths)
}
Test the Module End-to-End
Once your module has inputs, actions, and outputs, test it locally:
# From inside your repo
rt run evilcorp/utils/web/whoami -i target=https://example.com
Inspect recent runs:
rt audit --limit 5
rt output <run-id> --trace
rt output <run-id> --format json
If something looks off:
- fix the
.rt.hclfile - lean on your LSP diagnostics
- re-run
rt run ...until the behavior and outputs match your intent
When you’re happy:
- commit the module
- push it to your shared git remote
- other operators can
rt prep/rt syncand use it via CLI, API, or MCP AI tools.
