From membrowse
Integrates MemBrowse memory tracking into ELF binary projects via GitHub workflows, membrowse-targets.json config, and README badges. Supports embedded firmware (STM32, ESP32, nRF, RISC-V) and Linux x86/x64 targets.
npx claudepluginhub membrowse/membrowse-action --plugin membrowseThis skill is limited to using the following tools:
You are integrating MemBrowse memory analysis into a project that produces ELF binaries. This works for both embedded firmware (STM32, ESP32, nRF, RISC-V) and non-embedded targets (Linux x86/x64 applications, game engines, system software). Follow these steps to identify build targets, create the configuration file, set up GitHub workflows, and optionally add a MemBrowse badge to the README.
Bootstraps rekal memory for a project by scanning codebase for architecture, conventions, dependencies, workflows, and config, storing as typed, tagged, deduplicated memories. Trigger: /rekal-init.
Maps unfamiliar codebases in phases: structure, entry points, data flow, patterns, landmines. Use before coding in new, inherited, or revisited projects.
Analyzes codebase to produce knowledge-graph.json for interactive dashboard exploring architecture, components, and relationships
Share bugs, ideas, or general feedback.
You are integrating MemBrowse memory analysis into a project that produces ELF binaries. This works for both embedded firmware (STM32, ESP32, nRF, RISC-V) and non-embedded targets (Linux x86/x64 applications, game engines, system software). Follow these steps to identify build targets, create the configuration file, set up GitHub workflows, and optionally add a MemBrowse badge to the README.
Before exploring the codebase, gather project-level configuration. Auto-detect what you can, then ask the user only what requires their input.
# Detect submodules (non-empty output means submodules exist)
git submodule status
# Detect default branch
git symbolic-ref refs/remotes/origin/HEAD
Use AskUserQuestion to ask:
Understand what the project builds and how.
Use the Glob tool to find build configuration files:
**/Makefile***/CMakeLists.txt**/*.mk**/meson.build**/Cargo.tomlFor embedded projects, also search for board/port directories:
**/boards/**/ports/Check existing CI workflows by reading files in .github/workflows/.
Read existing workflow files to understand:
Linker scripts define memory regions (FLASH, RAM, etc.) and are mainly used in embedded projects. Non-embedded projects typically don't have custom linker scripts — this is fine, MemBrowse will use default Code/Data regions based on ELF sections.
Use the Glob tool to find linker scripts:
**/*.ld**/*.ldsUse the Grep tool to check Makefiles for linker script references:
LDSCRIPT|\.ld|-T in Makefile* and *.mk filesUse the Grep tool to search for ELF references in CI workflow files:
\.elf|\.out|firmware|build/ in .github/workflows/*.ymlCommon embedded patterns:
build/firmware.elfbuild-BOARDNAME/firmware.elfbuild/PROJECT_NAME.elfCommon non-embedded patterns:
build/myapp (no extension — use file command to confirm it's ELF)target/release/myapp (Rust)builddir/myapp (Meson)For each target you identify, gather:
| Field | Description |
|---|---|
target_name | Unique identifier (e.g., stm32-pybv10, esp32-devkit, linux-x64) |
setup_cmd | Commands to install build dependencies |
build_cmd | Commands to compile the project (include map file generation flags — see below) |
elf | Path to output ELF file after build |
ld | Space-separated linker script paths (can be empty) |
map_file | Path to the generated linker map file (empty if toolchain doesn't support it) |
linker_vars | Optional: variable definitions for linker parsing (e.g., "__flash_size__=4096K") |
Map files enable archive/object file attribution — MemBrowse can show which .a library or .o object file each symbol comes from. Map files are build artifacts generated by the linker, so the build_cmd must include the appropriate flags.
When constructing build_cmd, check if the project's toolchain supports map file generation and add the flag. Use the Grep tool to inspect Makefiles/CMakeLists.txt for the toolchain and existing linker flags (e.g., LDFLAGS, target_link_options, CMAKE_EXE_LINKER_FLAGS).
Toolchain-specific flags:
| Toolchain | Flag to add | Example map_file path |
|---|---|---|
| GCC / Clang (Make) | -Wl,-Map=path/to/output.map in LDFLAGS | build/firmware.map |
| GCC / Clang (CMake) | -DCMAKE_EXE_LINKER_FLAGS="-Wl,-Map=${PWD}/build/output.map" | build/output.map |
| IAR EWARM | Map files generated by default | Debug/Exe/project.map |
| ESP-IDF | Map file generated by default at build/<project>.map | build/<project>.map |
| Rust (Cargo) | RUSTFLAGS="-C link-args=-Wl,-Map=output.map" | output.map |
How to add map generation to the build command:
-Wl,-Map or has a MAP variable. If not, append the flag via a Make variable override, e.g., make BOARD=PYBV10 LDFLAGS_EXTRA="-Wl,-Map=build/firmware.map" — but check that the Makefile actually uses LDFLAGS_EXTRA or an equivalent. If the Makefile doesn't expose a way to add linker flags, the map file may need to be skipped for that target.-DCMAKE_EXE_LINKER_FLAGS="-Wl,-Map=${PWD}/build/output.map" at configure time.build/<project_name>.map. No flag changes needed — just set the map_file field.If the toolchain doesn't support map file generation or there's no clean way to add the flag to the build command, leave map_file empty — MemBrowse will still work, just without library-level attribution.
x86/x64 Linux (non-embedded):
sudo apt-get update && sudo apt-get install -y build-essential
# Add project-specific libraries as needed (e.g., libssl-dev, libffi-dev)
ARM Cortex-M (STM32, SAMD, NXP, etc.):
sudo apt-get update && sudo apt-get install -y gcc-arm-none-eabi libnewlib-arm-none-eabi
ESP32/ESP8266:
# Usually handled by project CI scripts or ESP-IDF setup
. $IDF_PATH/export.sh
RISC-V:
# Check project docs for specific toolchain
sudo apt-get update && sudo apt-get install -y gcc-riscv64-unknown-elf
Before building or verifying anything, present the discovered targets to the user for a quick sanity check. For each target, show:
Ask the user:
This catches obvious mistakes before investing time in Step 5 (local verification).
Before adding targets to the configuration file, verify each target end-to-end: build succeeds, ELF is found, and membrowse report produces valid output.
Note: If the required toolchain (e.g., gcc-arm-none-eabi, ESP-IDF) is not available locally, skip this step and defer verification to CI. Proceed to Step 6 and let the first workflow run validate the configuration. If the build is straightforward and the toolchain is available, local verification is strongly recommended — it catches issues much faster than round-tripping through CI.
pip install membrowse # if not already installed
For each target, run the build command and confirm the ELF file exists:
# Run the build
make clean && make BOARD=PYBV10 # embedded example
cmake -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo && cmake --build build # non-embedded example
# Confirm the ELF file exists
ls -la build/firmware.elf # embedded
file build/myapp # non-embedded — confirm "ELF" in output
If the ELF is not found, check the build output for the actual output path.
Run membrowse report on the built ELF to verify the full analysis pipeline. This is the single most important verification step — it catches bad ELF paths, unparseable linker scripts, missing linker variables, and unsupported binary formats all at once.
With linker scripts (embedded):
membrowse report path/to/firmware.elf "path/to/linker.ld"
Without linker scripts (non-embedded or no custom layout):
membrowse report path/to/binary
With map file (for library/object attribution):
membrowse report path/to/firmware.elf "path/to/linker.ld" --map-file path/to/output.map
With linker variable definitions:
membrowse report path/to/firmware.elf "path/to/linker.ld" --def __flash_size__=4096K
The human-readable output should show:
To inspect the full JSON output (useful for debugging):
membrowse report path/to/firmware.elf "path/to/linker.ld" --json
Check that the JSON contains:
memory_regions with non-zero used valuessymbols array (non-empty if built with -g debug symbols)sections array with expected ELF sections (.text, .data, .bss, etc.)--def VAR=VALUE. Note the value for the linker_vars field in targets.json.-g to compiler flags or use -DCMAKE_BUILD_TYPE=RelWithDebInfo. Sections and regions will still work, but source file attribution requires debug symbols.file path/to/binary to check. Look for the intermediate ELF before any objcopy conversion.membrowse report path/to/elf) to see default Code/Data analysis, then compare.If you cannot resolve a failure after trying the common fixes above, stop and ask the user for help using AskUserQuestion before proceeding. Do not skip a failing target or move on to creating configuration files with unverified targets.
Show the user the full human-readable membrowse report output for each target. This is the concrete proof that the target configuration works — memory regions, usage percentages, and any warnings are all visible here.
Ask the user to confirm:
-g flag)?Only proceed to create the configuration file after the user approves the membrowse report output.
Create .github/membrowse-targets.json with the verified targets. The file is a flat JSON array — each element is one target.
Embedded example (with map file):
[
{
"target_name": "stm32-pybv10",
"setup_cmd": "sudo apt-get update && sudo apt-get install -y gcc-arm-none-eabi",
"build_cmd": "make -C ports/stm32 BOARD=PYBV10 LDFLAGS_EXTRA=\"-Wl,-Map=build-PYBV10/firmware.map\"",
"elf": "ports/stm32/build-PYBV10/firmware.elf",
"ld": "ports/stm32/boards/stm32f405.ld",
"map_file": "ports/stm32/build-PYBV10/firmware.map",
"linker_vars": ""
}
]
Non-embedded example (CMake with map file):
[
{
"target_name": "linux-x64",
"setup_cmd": "sudo apt-get update && sudo apt-get install -y build-essential",
"build_cmd": "cmake -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_EXE_LINKER_FLAGS=\"-Wl,-Map=$PWD/build/myapp.map\" && cmake --build build",
"elf": "build/myapp",
"ld": "",
"map_file": "build/myapp.map",
"linker_vars": ""
}
]
ESP-IDF example (map file generated by default):
[
{
"target_name": "esp32",
"setup_cmd": ". $IDF_PATH/export.sh",
"build_cmd": "idf.py build",
"elf": "build/project_name.elf",
"ld": "build/esp-idf/esp32/esp32.project.ld",
"map_file": "build/project_name.map",
"linker_vars": ""
}
]
target_name: Must be unique across targets, used as the target identifier in MemBrowseelf: Path to any ELF binary — embedded firmware (.elf) or non-embedded executables/shared libraries (no extension or .so)ld: Space-separated linker script paths for embedded projects; empty string "" for non-embedded (analysis will use default Code/Data regions based on ELF sections like .text, .data, .bss)map_file: Path to the linker map file generated by the build command; empty string "" if the build doesn't produce one. Must match the path used in the -Wl,-Map= flag in build_cmd. Supports GNU LD and IAR EWARM formats.linker_vars: Only needed if linker scripts use undefined variables (e.g., "__flash_size__=4096K"); leave empty for non-embeddedsetup_cmd: Commands to install build dependencies before building (skill-specific; the workflow runs this before the build step)build_cmd: Use -g or -DCMAKE_BUILD_TYPE=RelWithDebInfo to include debug symbols — this lets MemBrowse attribute memory to source files and symbolsBased on the project type identified in Step 1, create workflows in .github/workflows/.
Choose the appropriate pattern:
workflow_runCreate one workflow file: membrowse.yml
Use this when there is only one target and the repo does not accept fork PRs.
name: MemBrowse Memory Report
on:
pull_request:
push:
branches:
- main # Change to match the project's default branch
permissions:
contents: read
pull-requests: write
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.sha || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
analyze:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
# Only include submodules line if user confirmed submodules in Step 1
# submodules: recursive
- name: Install packages
run: |
# TARGET_SETUP_CMD goes here
- name: Build
run: |
# TARGET_BUILD_CMD goes here
# For non-embedded: e.g., cmake -B build && cmake --build build
# For embedded: e.g., make -C ports/stm32 BOARD=PYBV10
- name: Run MemBrowse analysis
uses: membrowse/membrowse-action@v1
with:
target_name: TARGET_NAME
elf: TARGET_ELF
ld: TARGET_LD
# Only include map_file if the target has a map file
# map_file: TARGET_MAP_FILE
# Only include linker_vars if the target has linker_vars values
# linker_vars: TARGET_LINKER_VARS
api_key: ${{ secrets.MEMBROWSE_API_KEY }}
api_url: ${{ vars.MEMBROWSE_API_URL }}
verbose: INFO
# Uncomment to allow CI to pass even when memory budgets are exceeded
# dont_fail_on_alerts: true
- name: Post PR comment
if: github.event_name == 'pull_request'
uses: membrowse/membrowse-action/comment-action@v1
with:
api_key: ${{ secrets.MEMBROWSE_API_KEY }}
commit: ${{ github.event.pull_request.head.sha }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Template substitutions: Replace TARGET_NAME, TARGET_SETUP_CMD, TARGET_BUILD_CMD, TARGET_ELF, TARGET_LD, TARGET_MAP_FILE, and TARGET_LINKER_VARS with values from the single target in membrowse-targets.json. Since there's only one target, values are inlined directly.
Create one workflow file: membrowse.yml
Use this when there are multiple targets and the repo does not accept fork PRs.
name: MemBrowse Memory Report
on:
pull_request:
push:
branches:
- main # Change to match the project's default branch
permissions:
contents: read
pull-requests: write
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.sha || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
load-targets:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Load target matrix
id: set-matrix
run: echo "matrix=$(jq -c '.' .github/membrowse-targets.json)" >> $GITHUB_OUTPUT
analyze:
needs: load-targets
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.load-targets.outputs.matrix) }}
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
# Only include submodules line if user confirmed submodules in Step 1
# submodules: recursive
- name: Install packages
run: ${{ matrix.setup_cmd }}
- name: Build
run: ${{ matrix.build_cmd }}
- name: Run MemBrowse analysis
uses: membrowse/membrowse-action@v1
with:
target_name: ${{ matrix.target_name }}
elf: ${{ matrix.elf }}
ld: ${{ matrix.ld }}
map_file: ${{ matrix.map_file }}
linker_vars: ${{ matrix.linker_vars }}
api_key: ${{ secrets.MEMBROWSE_API_KEY }}
api_url: ${{ vars.MEMBROWSE_API_URL }}
verbose: INFO
# Uncomment to allow CI to pass even when memory budgets are exceeded
# dont_fail_on_alerts: true
comment:
needs: analyze
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Post combined PR comment
if: ${{ env.MEMBROWSE_API_KEY != '' }}
uses: membrowse/membrowse-action/comment-action@v1
with:
api_key: ${{ secrets.MEMBROWSE_API_KEY }}
commit: ${{ github.event.pull_request.head.sha }}
env:
MEMBROWSE_API_KEY: ${{ secrets.MEMBROWSE_API_KEY }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Create two workflow files: membrowse-report.yml and membrowse-comment.yml
Use this when the project accepts PRs from forks. Fork PRs don't have access to secrets, so the report workflow uploads data to the MemBrowse API, and a separate workflow_run-triggered workflow fetches the summary and posts the PR comment.
name: MemBrowse Memory Report
on:
pull_request:
push:
branches:
- main # Change to match the project's default branch
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.sha || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
load-targets:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Load target matrix
id: set-matrix
run: echo "matrix=$(jq -c '.' .github/membrowse-targets.json)" >> $GITHUB_OUTPUT
analyze:
needs: load-targets
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.load-targets.outputs.matrix) }}
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
# Only include submodules line if user confirmed submodules in Step 1
# submodules: recursive
- name: Install packages
run: ${{ matrix.setup_cmd }}
- name: Build
run: ${{ matrix.build_cmd }}
- name: Run MemBrowse analysis
uses: membrowse/membrowse-action@v1
with:
target_name: ${{ matrix.target_name }}
elf: ${{ matrix.elf }}
ld: ${{ matrix.ld }}
map_file: ${{ matrix.map_file }}
linker_vars: ${{ matrix.linker_vars }}
api_key: ${{ secrets.MEMBROWSE_API_KEY }}
api_url: ${{ vars.MEMBROWSE_API_URL }}
verbose: INFO
# Uncomment to allow CI to pass even when memory budgets are exceeded
# dont_fail_on_alerts: true
IMPORTANT: The workflows: value below must exactly match the name: field of the report workflow above (MemBrowse Memory Report). If they don't match, the comment workflow will never trigger.
name: MemBrowse PR Comment
on:
workflow_run:
workflows: [MemBrowse Memory Report]
types:
- completed
jobs:
comment:
runs-on: ubuntu-latest
# Run even if some builds failed — only skip if the run was cancelled
if: >
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion != 'cancelled'
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Post combined PR comment
if: ${{ env.MEMBROWSE_API_KEY != '' }}
uses: membrowse/membrowse-action/comment-action@v1
with:
api_key: ${{ secrets.MEMBROWSE_API_KEY }}
commit: ${{ github.event.workflow_run.head_sha }}
env:
MEMBROWSE_API_KEY: ${{ secrets.MEMBROWSE_API_KEY }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Always create a separate membrowse-onboard.yml workflow for historical analysis:
name: Onboard to MemBrowse
on:
workflow_dispatch:
inputs:
num_commits:
description: 'Number of commits to process'
required: true
default: '100'
type: string
jobs:
load-targets:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Load target matrix
id: set-matrix
run: echo "matrix=$(jq -c '.' .github/membrowse-targets.json)" >> $GITHUB_OUTPUT
onboard:
needs: load-targets
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.load-targets.outputs.matrix) }}
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
# Only include submodules line if user confirmed submodules in Step 1
# submodules: recursive
- name: Install packages
run: ${{ matrix.setup_cmd }}
- name: Run MemBrowse Onboard Action
uses: membrowse/membrowse-action/onboard-action@v1
with:
target_name: ${{ matrix.target_name }}
num_commits: ${{ github.event.inputs.num_commits }}
build_script: ${{ matrix.build_cmd }}
elf: ${{ matrix.elf }}
ld: ${{ matrix.ld }}
map_file: ${{ matrix.map_file }}
linker_vars: ${{ matrix.linker_vars }}
binary_search: 'true'
api_key: ${{ secrets.MEMBROWSE_API_KEY }}
api_url: ${{ vars.MEMBROWSE_API_URL }}
After creating the files, tell the user they need to configure:
Repository Secret: MEMBROWSE_API_KEY — API key from the MemBrowse dashboard
Location: Repository Settings → Secrets and variables → Actions → New repository secret
Repository Variable (optional): MEMBROWSE_API_URL — custom API URL if not using the default (https://api.membrowse.com)
Location: Repository Settings → Secrets and variables → Actions → Variables → New repository variable
Tell the user how to test:
Before adding a badge, ask the user:
If the project is private and public access is not enabled, skip the badge — it will return a 404. Inform the user they can enable public access later in Project Settings on the MemBrowse portal.
Use the Glob tool to find README files:
README*readme*The badge URL format is:
[](https://membrowse.com/public/{owner}/{repo})
Get the owner and repo name from the git remote:
git remote get-url origin
Parse the URL to extract {owner}/{repo} (e.g., micropython/micropython).
Add the badge near the top of the README, typically:
Example placement:
# Project Name
[](https://membrowse.com/public/owner/repo)
Project description here...
If other badges exist, add it inline:
# Project Name
[](...)
[](...)
[](https://membrowse.com/public/owner/repo)
Before modifying the README:
After the integration is working, mention these optional features the user can explore:
dont_fail_on_alerts: true in the action to prevent CI failure on budget alerts.build_dirs (only rebuild when specific directories change) and initial_commit (start from a specific commit hash).identical input with a paths-filter action to skip MemBrowse analysis on commits that don't touch source code (e.g., docs-only changes).comment_template in the comment action to customize the PR comment format with a Jinja2 template.Ensure the binary is compiled with debug symbols (-g flag, or -DCMAKE_BUILD_TYPE=RelWithDebInfo for CMake). Without debug info, MemBrowse can still report section-level usage but cannot attribute memory to individual source files and symbols.
pull-requests: write permission.workflow_run.workflows: value in membrowse-comment.yml must exactly match the name: field in membrowse-report.yml (i.e., "MemBrowse Memory Report").MEMBROWSE_API_KEY in repository settings.workflow_run comment workflow handles this by posting comments from the base repo context.Ensure fetch-depth: 0 is set on the checkout step. Without full history, MemBrowse cannot detect branch and commit metadata correctly.
MemBrowse requires ELF binaries. If your build produces .bin, .hex, or other non-ELF formats, locate the intermediate ELF file (usually in the build directory before the final conversion step). For non-embedded projects, the output binary is typically already ELF — verify with file path/to/binary (should show "ELF").
setup_cmdsubmodules: recursive on the checkout steplinker_vars in membrowse-targets.jsonld field is valid — analysis will use default Code/Data regions