Skip to content

GitHub Action (Mistral Vibe)

Mistral Vibe is a tool-calling agent framework; action-mistral-vibe is a composite GitHub Action that runs one inside your workflow. Point it at OpenTremor’s MCP endpoint and it handles the whole loop itself — ingest the plan, work through each resource, and leave a security report as a PR comment — with no glue code in your workflow beyond the plan-generation step.

Don’t need CI at all? The GitHub App integration analyses every PR automatically the moment it’s installed — no workflow file, and a free choice between anthropic, openai, or mistral as the backend. Reach for this Action instead when you need CI-side control: running terraform plan yourself first (as below), tuning the agent’s prompt directly, or slotting this into an existing Mistral Vibe-based CI setup. Nothing about the pattern here is Mistral-specific, either — any CI action that wraps a tool-calling model can drive the same MCP endpoints the same way; this is just the one with a ready-made Action already built.

sequenceDiagram
    participant CI as GitHub Actions
    participant Action as action-mistral-vibe
    participant Vibe as Mistral Vibe (Devstral)
    participant API as OpenTremor Core

    CI->>Action: prompt + plan text + MCP server config
    Action->>Vibe: launch agent, MCP tools attached

    Vibe->>API: ingest(namespace, plan_text)
    API-->>Vibe: analysis units[]

    loop until HTTP 208
        Vibe->>API: get_next_resource(namespace)
        API-->>Vibe: unit body + inline rules
        Note over Vibe: evaluate body against rules
        Vibe->>API: submit_analysis(hash, findings)
    end

    Vibe->>API: generate_report(namespace)
    API-->>Vibe: consolidated report
    Action->>CI: comment the report on the PR
  • An OpenTremor Core deployment your CI runner can reach (see Deployment)
  • A Mistral API key with tool-use access (devstral-2 or similar)
  • action-mistral-vibe available to your organisation
name: Terraform security analysis
on:
pull_request:
paths:
- "**.tf"
- "**.tfvars"
permissions:
id-token: write # needed for OIDC -> AWS role assumption
contents: read
pull-requests: write # so the action can leave a PR comment
jobs:
terraform-security:
runs-on: prl-self-hosted
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Assume the plan/read role via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ vars.AWS_OIDC_ROLE_ARN }}
aws-region: ${{ vars.AWS_REGION }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
- name: Render the plan as text
id: plan
run: |
terraform init -input=false
terraform plan -out=tfplan -input=false
terraform show -no-color tfplan > plan.txt
echo "stdout<<EOF" >> $GITHUB_OUTPUT
cat plan.txt >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
working-directory: ${{ vars.TF_WORKING_DIR }} # e.g. terraform/envs/staging/
- name: Run the analysis agent
id: analyse
uses: your-org/action-mistral-vibe@main
with:
prompt: |
You have access to the OpenTremor MCP server ("terraform-analyzer"). Use it to
review the Terraform plan below for security issues. Work through these steps
in order and don't skip any of them:
1. Call ingest(analyzer_name="terraform-plan", namespace="pr-${{ github.run_id }}", raw_input=<the plan text pasted below>).
This returns a JSON array of units. If it's empty, stop and report that nothing was found to analyse.
2. Loop:
a. Call get_next_resource(namespace="pr-${{ github.run_id }}").
- A 208 status means every unit has been analysed — move on to step 3.
- Otherwise the response carries both the unit's body and the rules to check it against.
b. Evaluate the body against those rules.
c. Call submit_analysis(hash=<the unit's hash>, <your findings>).
d. Go back to 2a.
3. Call generate_report(namespace="pr-${{ github.run_id }}", format="markdown_light") and return
its output verbatim as your final message — nothing added before or after it. It already
contains a link to the full HTML report, which needs no authentication to open.
Plan:
${{ steps.plan.outputs.stdout }}
config_toml: |
active_model = "devstral-2"
[[providers]]
name = "mistral"
api_base = "https://codestral.mistral.ai/v1"
api_key_env_var = "ENDPOINT_API_KEY"
api_style = "openai"
backend = "mistral"
[[models]]
name = "mistral-vibe-cli-latest"
provider = "mistral"
alias = "devstral-2"
temperature = 0.2
input_price = 0.4
output_price = 2.0
[[mcp_servers]]
name = "terraform-analyzer"
transport = "streamable-http"
url = "${{ vars.MCP_BASE_URL }}/mcp/"
headers = { "X-API-Key" = "${{ secrets.MCP_API_KEY }}" }
post_raw_output: "true"
comment_header: "## OpenTremor security analysis"
max_price: "0.50"
timeout: "300"
env:
ENDPOINT_API_KEY: ${{ secrets.MISTRAL_API_KEY }}

Secrets and variables this workflow expects

Section titled “Secrets and variables this workflow expects”
NameKindWhat it’s for
MISTRAL_API_KEYSecretforwarded to the action as ENDPOINT_API_KEY
MCP_API_KEYSecretOpenTremor API key
AWS_OIDC_ROLE_ARNVariablerole the OIDC step assumes for Terraform state access
AWS_REGIONVariablee.g. eu-west-1
MCP_BASE_URLVariablewhere your OpenTremor deployment lives, e.g. https://analyzer.example.com
TF_WORKING_DIRVariableTerraform root to plan, e.g. terraform/envs/staging/
InputWhat it controlsExample
promptthe instructions driving the whole run, plan text included inlinesee above
config_tomlVibe’s own config — MCP server URL, auth header, modelsee above
post_pr_commentwhether the final report gets posted as a PR comment"true"
comment_headerheading text on that comment"## OpenTremor security analysis"
max_pricedollar ceiling before the run is aborted"0.50"
timeoutseconds before the agent is killed"300"
publish_reportalso push an HTML copy to GitHub Pages"true"
always_runrerun even if this SHA already has a passing result"false"

Whatever you put in config_toml is dropped straight into Vibe’s own config.toml — the [[mcp_servers]] block is what tells it OpenTremor exists and how to reach it:

[[mcp_servers]]
name = "terraform-analyzer" # namespace the LLM sees these tools under
transport = "streamable-http"
url = "https://analyzer.example.com/mcp/"
headers = { "X-API-Key" = "your-user-key" } # drop this line if auth is off

Vibe introspects the server at startup and picks up every tool it exposes — get_rules, ingest, get_next_resource, submit_analysis, generate_report, and so on — there’s no separate tool list to maintain.

Running plan and code-change analysis together

Section titled “Running plan and code-change analysis together”

Ingesting both a plan and a diff into the same namespace lets one agent run cover both — each unit’s metadata.analyzer tells it which ruleset applies, so the loop itself doesn’t change at all:

- name: Render plan and diff
run: |
terraform init -input=false
terraform plan -out=tfplan -input=false
terraform show -no-color tfplan > /tmp/plan.txt
git diff origin/main -- '*.tf' > /tmp/diff.txt
- name: Run the analysis agent
uses: your-org/action-mistral-vibe@main
with:
prompt: |
Using the terraform-analyzer MCP server, run both a plan analysis and a
code-change analysis in this same session.
1. Ingest both inputs into one namespace:
a. ingest(analyzer_name="terraform-plan", namespace="pr-${{ github.run_id }}", raw_input=<contents of /tmp/plan.txt>)
b. ingest(analyzer_name="terraform-code-change", namespace="pr-${{ github.run_id }}", raw_input=<contents of /tmp/diff.txt>)
2. Drain the queue:
a. get_next_resource(namespace="pr-${{ github.run_id }}")
- 208 means everything's analysed — go to step 3.
- otherwise you get the unit's body plus the rules to check it with.
b. Evaluate the body against those rules.
c. submit_analysis(hash=<the unit's hash>, <your findings>)
d. Repeat from 2a.
3. generate_report(namespace="pr-${{ github.run_id }}") and return its
output verbatim as your final message.
config_toml: |
...

One report comes out the other end covering the plan’s security findings alongside the diff’s compliance findings.

A concatenated multi-stack terramate run terraform show -no-color tfplan run doesn’t need any special handling — hand it to ingest exactly as produced. The parser recognises each terramate: Entering stack in <path> marker and tags every unit with the stack it came from, and the rendered report picks up a Stack column automatically once at least one unit carries that tag — so a finding can always be traced back to the stack that introduced it.

Terminal window
# All stacks, one file
terramate run terraform show -no-color tfplan > /tmp/full_plan.txt

Feed that file’s contents into the prompt the same way as a single-stack plan (see Large plans below if it’s too big to inline).

If a plan is too big for the action’s prompt character limit, write it to a file and have the agent read from there instead:

- name: Save plan to file
run: terraform show -no-color tfplan > /tmp/plan.txt
- name: Analyse
uses: your-org/action-mistral-vibe@main
with:
prompt: |
Read /tmp/plan.txt and analyse it for security issues using the
terraform-analyzer MCP tools: ingest it, then loop get_next_resource +
submit_analysis until done, then generate_report.
config_toml: |
[[mcp_servers]]
name = "terraform-analyzer"
transport = "streamable-http"
url = "${{ vars.MCP_BASE_URL }}/mcp/"

max_price aborts the run once estimated spend crosses the threshold you set. A typical 10-30 resource plan against devstral-2 runs somewhere around $0.05-$0.20.

max_price: "0.30" # comfortable ceiling for most PRs

Full token and cost accounting is available afterward via ${{ steps.<id>.outputs.result }}.

The action remembers a successful run per commit SHA by default — pushing again to the same SHA posts a “skipped, already analysed” comment instead of re-running the (paid) LLM loop. Pass always_run: "true" if you want it to run anyway every time.

OutputContents
statussuccess or failure
resultfull JSON — findings, token counts, cost
execution_timehow long the run took, in seconds
error_messagewhy it failed, if it did

Wire these into later steps as needed:

- name: Fail loudly if the analysis errored
if: steps.analyse.outputs.status == 'failure'
run: echo "Analysis failed: ${{ steps.analyse.outputs.error_message }}"