The 5 Client Communication Mistakes That Kill Freelance Careers

Most freelancers don’t lose clients because of bad work. They lose them because of how they communicate. Clients will tolerate imperfect deliverables if they trust you. They won’t tolerate silence, vague updates, or feeling like they’re chasing you down.

Here are the five communication patterns that quietly destroy freelance careers — and what to do instead.

Mistake 1: Going Quiet During Projects

This is the most common one. You’re deep in a project, making progress, and you assume no news is good news. Meanwhile, the client is wondering if you’re still alive.

Clients don’t have visibility into your process. They don’t know if the design is coming along beautifully or if you’re stuck and embarrassed to say so. Silence activates their worst assumptions.

The fix: Set a weekly update cadence whether or not you have something to show. Even a two-sentence email — “Working through the API integration today, on track for the Thursday delivery” — does enormous work. It signals competence, professionalism, and that they don’t need to babysit you.

Use a template if it helps:

“Quick update on [project]: [what I did this week], [what’s next], [still on track / here’s a note on timeline]. No action needed from you.”

Send it every Monday morning. Your clients will start trusting you in ways they can’t articulate.

Mistake 2: Slow Response Times

Not always-available fast — but basic-professional fast. If a client emails you on a Wednesday afternoon, they should have a response by end of business Thursday at the latest. More than 48 hours of silence for a direct question is a red flag.

Slow responses communicate that clients are not a priority. Even if you’re busy and the full answer will take time, a quick acknowledgment — “Got this, will have a full response for you by Friday” — prevents anxiety.

The fix: Batch your email twice a day (morning and afternoon) and respond to everything in those windows. If something needs more research, acknowledge it and give a timeline. Never let a client email go unanswered for more than 24-36 hours without a brief acknowledgment.

Mistake 3: Unclear Deliverables and Scope

“I’ll build you a website” is not a deliverable. Neither is “I’ll help with your marketing.” Vague scope leads to scope creep, resentment, and arguments about whether you delivered what you promised.

The most common version of this mistake: starting work before the scope is in writing. Verbal agreements feel fine at the time and become problems when expectations diverge.

The fix: Every project needs a written scope before work begins. It doesn’t need to be a formal contract (though one doesn’t hurt) — a simple bullet list in an email confirmation works:

“Confirming what’s included in this project: [bullet list]. Not included in this scope: [bullet list]. Delivery date: [date]. Any changes to scope will be quoted separately.”

This protects you AND the client. Clear expectations are how professional relationships stay professional.

Mistake 4: Delivering Without Context

You send over the design files. You send over the finished code. You send over the report. And you… send it with no explanation.

Even great work lands flat when clients have to figure out what they’re looking at. They feel like they’re being handed something and told to deal with it.

The fix: Every delivery should include a brief explanation of what you built, why you made the key decisions you made, and what the next step is. It doesn’t have to be long:

“Attached is the completed website design. Three things to note: [decision 1 and why], [decision 2 and why], [decision 3 and why]. Next step is your feedback by [date] — I’ve left placeholder text in two sections that need your input.”

This turns a file transfer into a professional handoff. It demonstrates that you thought about what they need, not just that you completed a task.

Mistake 5: Avoiding Difficult Conversations

Something went wrong. The project is running late. You made an error. There’s a scope issue that needs addressing. The instinct is to delay, minimize, or just hope it resolves itself.

It never resolves itself. Problems that aren’t addressed directly become resentments. Resentments end client relationships.

The fix: Address problems early and directly. Most clients are more reasonable than you fear — they want to work with professionals who tell them the truth, not hide from it.

The formula: acknowledge the issue, explain briefly what happened, provide the path forward with a concrete timeline.

“I need to flag something on [project]. [What happened, one sentence]. This means [impact on timeline/scope]. My plan to address it: [specific steps]. New delivery date: [date]. Let me know if you want to discuss.”

A client who hears this promptly and professionally will often respect you more than before the problem happened. A client who doesn’t hear from you for a week and then discovers the problem themselves will not.

The Underlying Principle

Good client communication isn’t about being perfect. It’s about giving clients confidence that they know what’s happening and that you’re handling it.

Regular updates, clear scope, fast responses, professional deliveries, and honest problem-solving — these are table stakes for a sustainable freelance career.

Looking to systematize your freelance workflow? The Freelancer AI Toolkit includes AI prompt templates for client communication, project kickoffs, and handling difficult conversations.

ABAP OOP Design Patterns — Part 2: Factory, Observer, and Decorator Patterns in Real SAP Systems

ABAP OOP Design Patterns — Part 2: Factory, Observer, and Decorator Patterns in Real SAP Systems
If you’ve been writing ABAP long enough, you’ve probably inherited a codebase where every business rule is buried inside a 3,000-line function module, and adding a new requirement means copy-pasting logic you’re not even sure is correct. That’s not a skills problem — it’s an architecture problem. In Part 1 of this series, we explored how the Strategy Pattern helps you swap business logic cleanly without touching the calling code. Now it’s time to go further. In this second installment, we’re tackling three more battle-tested ABAP OOP design patterns: Factory, Observer, and Decorator. Each one solves a very specific pain point I’ve encountered repeatedly across large SAP S/4HANA implementations — and I’ll show you exactly how to apply them.

Before we dive in, a quick note: these patterns aren’t academic exercises. Every example below is inspired by real implementation challenges on production SAP systems. If you’re also working on improving code quality across the board, it’s worth reading alongside our guides on Clean ABAP best practices and robust exception handling in ABAP.

Why These Three Patterns Matter in SAP Environments

SAP systems are notorious for their complexity — multiple integration touchpoints, constantly changing business rules, and the eternal challenge of extending standard functionality without breaking things. Design patterns give you a shared vocabulary and proven blueprints for solving these recurring structural problems.

Here’s a quick orientation before we get into code:

  • Factory Pattern: Controls how objects are created, keeping instantiation logic away from business logic.

  • Observer Pattern: Decouples event producers from event consumers — critical in event-driven SAP architectures.

  • Decorator Pattern: Adds behavior to objects dynamically without changing the original class — your best friend when extending standard SAP logic.

Let’s get into each one.

Pattern 1: The Factory Pattern — Stop Hardcoding Object Creation

The Problem It Solves

Imagine you have a pricing engine that needs to instantiate different pricing strategies based on customer type: standard, VIP, or wholesale. Without a factory, you’ll see code like this scattered everywhere:


" The anti-pattern: instantiation logic bleeding into business logic
IF lv_customer_type = 'VIP'.
  CREATE OBJECT lo_pricing TYPE zcl_vip_pricing.
ELSEIF lv_customer_type = 'WHOLESALE'.
  CREATE OBJECT lo_pricing TYPE zcl_wholesale_pricing.
ELSE.
  CREATE OBJECT lo_pricing TYPE zcl_standard_pricing.
ENDIF.

Now imagine this block duplicated across 12 different programs. The day you add a new customer type, you’re hunting through the entire codebase. That’s exactly the problem the Factory Pattern eliminates.

Implementing a Simple Factory in ABAP

First, define your interface:


INTERFACE zif_pricing_strategy.
  METHODS:
    calculate_price
      IMPORTING iv_base_price   TYPE p DECIMALS 2
                iv_quantity     TYPE i
      RETURNING VALUE(rv_final_price) TYPE p DECIMALS 2.
ENDINTERFACE.

Then implement your concrete classes:


" Standard pricing: no discount
CLASS zcl_standard_pricing DEFINITION PUBLIC FINAL.
  PUBLIC SECTION.
    INTERFACES zif_pricing_strategy.
ENDCLASS.

CLASS zcl_standard_pricing IMPLEMENTATION.
  METHOD zif_pricing_strategy~calculate_price.
    rv_final_price = iv_base_price * iv_quantity.
  ENDMETHOD.
ENDCLASS.

" VIP pricing: 15% discount
CLASS zcl_vip_pricing DEFINITION PUBLIC FINAL.
  PUBLIC SECTION.
    INTERFACES zif_pricing_strategy.
ENDCLASS.

CLASS zcl_vip_pricing IMPLEMENTATION.
  METHOD zif_pricing_strategy~calculate_price.
    rv_final_price = iv_base_price * iv_quantity * '0.85'.
  ENDMETHOD.
ENDCLASS.

Now, the factory class itself — this is where the magic lives:


CLASS zcl_pricing_factory DEFINITION PUBLIC FINAL CREATE PRIVATE.
  PUBLIC SECTION.
    CLASS-METHODS:
      get_instance
        RETURNING VALUE(ro_factory) TYPE REF TO zcl_pricing_factory,
      create_strategy
        IMPORTING iv_customer_type        TYPE char10
        RETURNING VALUE(ro_strategy)      TYPE REF TO zif_pricing_strategy
        RAISING   zcx_unknown_customer_type.

  PRIVATE SECTION.
    CLASS-DATA: go_instance TYPE REF TO zcl_pricing_factory.
ENDCLASS.

CLASS zcl_pricing_factory IMPLEMENTATION.

  METHOD get_instance.
    " Singleton: only one factory instance needed
    IF go_instance IS NOT BOUND.
      CREATE OBJECT go_instance.
    ENDIF.
    ro_factory = go_instance.
  ENDMETHOD.

  METHOD create_strategy.
    CASE iv_customer_type.
      WHEN 'STANDARD'.
        CREATE OBJECT ro_strategy TYPE zcl_standard_pricing.
      WHEN 'VIP'.
        CREATE OBJECT ro_strategy TYPE zcl_vip_pricing.
      WHEN 'WHOLESALE'.
        CREATE OBJECT ro_strategy TYPE zcl_wholesale_pricing.
      WHEN OTHERS.
        " Always raise a typed exception — see our exception handling guide
        RAISE EXCEPTION TYPE zcx_unknown_customer_type
          EXPORTING iv_customer_type = iv_customer_type.
    ENDCASE.
  ENDMETHOD.

ENDCLASS.

Now your business code becomes clean and future-proof:


DATA: lo_factory  TYPE REF TO zcl_pricing_factory,
      lo_strategy TYPE REF TO zif_pricing_strategy.

lo_factory = zcl_pricing_factory=>get_instance( ).

TRY.
  lo_strategy = lo_factory->create_strategy( iv_customer_type = lv_cust_type ).
  DATA(lv_price) = lo_strategy->calculate_price(
    iv_base_price = '100.00'
    iv_quantity   = 5
  ).
CATCH zcx_unknown_customer_type INTO DATA(lx_exc).
  " Log and handle gracefully
ENDTRY.

Adding a new customer type now means adding one class and one WHEN clause in the factory. Nothing else changes. That’s the power of encapsulated object creation.

Pattern 2: The Observer Pattern — Event-Driven Logic Without Tight Coupling

The Problem It Solves

Consider a sales order creation process. When an order is created, you might need to: send a notification email, update a reporting table, and trigger a downstream MES workflow. Without the Observer pattern, your order creation class ends up calling all of these directly — a violation of the Single Responsibility Principle and a maintenance nightmare.

The Observer pattern lets you define a subject (the order) and observers (notification, reporting, MES integration) that register themselves and react independently.

ABAP Implementation


" Observer interface — all listeners must implement this
INTERFACE zif_order_observer.
  METHODS:
    on_order_created
      IMPORTING is_order_data TYPE zs_sales_order.
ENDINTERFACE.

" Subject interface — the observable entity
INTERFACE zif_order_subject.
  METHODS:
    attach IMPORTING io_observer TYPE REF TO zif_order_observer,
    detach IMPORTING io_observer TYPE REF TO zif_order_observer,
    notify IMPORTING is_order_data TYPE zs_sales_order.
ENDINTERFACE.


" Concrete subject: Sales Order processor
CLASS zcl_sales_order_processor DEFINITION PUBLIC.
  PUBLIC SECTION.
    INTERFACES zif_order_subject.
    METHODS create_order
      IMPORTING is_order_data TYPE zs_sales_order.
  PRIVATE SECTION.
    DATA: gt_observers TYPE TABLE OF REF TO zif_order_observer.
ENDCLASS.

CLASS zcl_sales_order_processor IMPLEMENTATION.

  METHOD zif_order_subject~attach.
    APPEND io_observer TO gt_observers.
  ENDMETHOD.

  METHOD zif_order_subject~detach.
    DELETE gt_observers WHERE table_line = io_observer.
  ENDMETHOD.

  METHOD zif_order_subject~notify.
    LOOP AT gt_observers INTO DATA(lo_observer).
      lo_observer->on_order_created( is_order_data = is_order_data ).
    ENDLOOP.
  ENDMETHOD.

  METHOD create_order.
    " Core order creation logic here...
    " (database insert, number range, etc.)

    " Notify all registered observers
    zif_order_subject~notify( is_order_data = is_order_data ).
  ENDMETHOD.

ENDCLASS.


" Concrete Observer 1: Email notification
CLASS zcl_order_email_notifier DEFINITION PUBLIC FINAL.
  PUBLIC SECTION.
    INTERFACES zif_order_observer.
ENDCLASS.

CLASS zcl_order_email_notifier IMPLEMENTATION.
  METHOD zif_order_observer~on_order_created.
    " Send confirmation email logic
    " cl_bcs or custom email class here
    WRITE: / 'Email sent for order:', is_order_data-order_id.
  ENDMETHOD.
ENDCLASS.

" Concrete Observer 2: MES trigger
CLASS zcl_order_mes_trigger DEFINITION PUBLIC FINAL.
  PUBLIC SECTION.
    INTERFACES zif_order_observer.
ENDCLASS.

CLASS zcl_order_mes_trigger IMPLEMENTATION.
  METHOD zif_order_observer~on_order_created.
    " Trigger MES workflow via REST call or IDoc
    " See: SAP MES Integration architecture article
    WRITE: / 'MES workflow triggered for order:', is_order_data-order_id.
  ENDMETHOD.
ENDCLASS.

Wiring it all together:


DATA: lo_processor  TYPE REF TO zcl_sales_order_processor,
      lo_email      TYPE REF TO zcl_order_email_notifier,
      lo_mes        TYPE REF TO zcl_order_mes_trigger.

CREATE OBJECT lo_processor.
CREATE OBJECT lo_email.
CREATE OBJECT lo_mes.

" Register observers
lo_processor->attach( lo_email ).
lo_processor->attach( lo_mes ).

" Create order — observers fire automatically
lo_processor->create_order( is_order_data = ls_order ).

Want to add a reporting observer next month? Create the class, register it. The processor never changes. This is exactly the kind of extensibility you need in a living SAP system — and it pairs beautifully with the event-driven approach we discussed in the context of SAP BTP Event Mesh architectures.

Pattern 3: The Decorator Pattern — Extending Behavior Without Inheritance Hell

The Problem It Solves

Here’s a scenario I see regularly: you have a report output class that formats data. Now stakeholders want optional features — logging, caching, and access control — applied in different combinations. Using inheritance, you’d need a class for every combination: LoggingCachingOutputFormatter, CachingOutputFormatter, etc. That explodes fast.

The Decorator pattern wraps objects to add behavior dynamically, without subclassing. It’s composable, testable, and elegant.

ABAP Implementation


" Core interface
INTERFACE zif_data_formatter.
  METHODS:
    format_data
      IMPORTING it_raw_data          TYPE ztt_report_data
      RETURNING VALUE(rv_output)     TYPE string.
ENDINTERFACE.

" Base concrete implementation
CLASS zcl_base_formatter DEFINITION PUBLIC.
  PUBLIC SECTION.
    INTERFACES zif_data_formatter.
ENDCLASS.

CLASS zcl_base_formatter IMPLEMENTATION.
  METHOD zif_data_formatter~format_data.
    " Core formatting logic — converts internal table to string output
    LOOP AT it_raw_data INTO DATA(ls_row).
      CONCATENATE rv_output ls_row-field1 '|' ls_row-field2 CL_ABAP_CHAR_UTILITIES=>NEWLINE
        INTO rv_output.
    ENDLOOP.
  ENDMETHOD.
ENDCLASS.


" Abstract decorator base — holds a reference to the wrapped component
CLASS zcl_formatter_decorator DEFINITION PUBLIC ABSTRACT.
  PUBLIC SECTION.
    INTERFACES zif_data_formatter.
    METHODS constructor
      IMPORTING io_wrapped TYPE REF TO zif_data_formatter.
  PROTECTED SECTION.
    DATA: mo_wrapped TYPE REF TO zif_data_formatter.
ENDCLASS.

CLASS zcl_formatter_decorator IMPLEMENTATION.
  METHOD constructor.
    mo_wrapped = io_wrapped.
  ENDMETHOD.
  METHOD zif_data_formatter~format_data.
    " Default: delegate to the wrapped component
    rv_output = mo_wrapped->format_data( it_raw_data = it_raw_data ).
  ENDMETHOD.
ENDCLASS.


" Logging decorator: wraps any formatter and adds execution logging
CLASS zcl_logging_formatter DEFINITION PUBLIC INHERITING FROM zcl_formatter_decorator FINAL.
  PUBLIC SECTION.
    METHODS zif_data_formatter~format_data REDEFINITION.
ENDCLASS.

CLASS zcl_logging_formatter IMPLEMENTATION.
  METHOD zif_data_formatter~format_data.
    DATA(lv_start) = cl_abap_systime=>get_current_time( ).

    " Delegate to wrapped formatter
    rv_output = mo_wrapped->format_data( it_raw_data = it_raw_data ).

    DATA(lv_end) = cl_abap_systime=>get_current_time( ).
    " Log execution time to application log
    MESSAGE |Formatter executed in { lv_end - lv_start } ms| TYPE 'I'.
  ENDMETHOD.
ENDCLASS.

" Caching decorator: returns cached output if data hasn't changed
CLASS zcl_caching_formatter DEFINITION PUBLIC INHERITING FROM zcl_formatter_decorator FINAL.
  PUBLIC SECTION.
    METHODS zif_data_formatter~format_data REDEFINITION.
  PRIVATE SECTION.
    DATA: mv_cache      TYPE string,
          mv_cache_key  TYPE string.
ENDCLASS.

CLASS zcl_caching_formatter IMPLEMENTATION.
  METHOD zif_data_formatter~format_data.
    " Simple hash-based cache key from row count + first key field
    DATA(lv_key) = |{ lines( it_raw_data ) }|.

    IF lv_key = mv_cache_key AND mv_cache IS NOT INITIAL.
      rv_output = mv_cache.  " Return cached result
      RETURN.
    ENDIF.

    rv_output = mo_wrapped->format_data( it_raw_data = it_raw_data ).
    mv_cache     = rv_output.
    mv_cache_key = lv_key.
  ENDMETHOD.
ENDCLASS.

Now compose them however you need, at runtime:


" Build a formatter stack: Base → Cache → Log
DATA: lo_base    TYPE REF TO zif_data_formatter,
      lo_cached  TYPE REF TO zif_data_formatter,
      lo_logged  TYPE REF TO zif_data_formatter.

CREATE OBJECT lo_base   TYPE zcl_base_formatter.
CREATE OBJECT lo_cached TYPE zcl_caching_formatter EXPORTING io_wrapped = lo_base.
CREATE OBJECT lo_logged TYPE zcl_logging_formatter EXPORTING io_wrapped = lo_cached.

" The caller uses the interface — unaware of what's underneath
DATA(lv_output) = lo_logged->format_data( it_raw_data = lt_data ).

Swap decorators in and out based on configuration. Add an access-control decorator for certain users. None of the underlying classes change. This is composition over inheritance in action — one of the most important principles in the Clean ABAP guidelines.

When to Use Which Pattern — A Decision Guide

Pattern
Use When
Avoid When

Factory
Object creation logic is complex or type-dependent
You only ever create one type of object

Observer
One event triggers multiple independent reactions
Observers are tightly ordered and dependent on each other

Decorator
You need to combine behaviors at runtime without subclassing
Behavior combinations are fixed and few

Making Patterns Testable with ABAP Unit

One underrated benefit of these patterns is testability. Because each component depends on interfaces, not concrete classes, you can inject test doubles easily. If you’re not yet writing unit tests for your ABAP classes, now is the time — our dedicated guide on ABAP unit testing in SAP S/4HANA walks through exactly how to structure tests for OOP-based code.

For example, testing the Observer pattern is trivial with a mock observer:


CLASS zcl_mock_observer DEFINITION FOR TESTING.
  PUBLIC SECTION.
    INTERFACES zif_order_observer.
    DATA: mv_was_called TYPE abap_bool,
          ms_received   TYPE zs_sales_order.
ENDCLASS.

CLASS zcl_mock_observer IMPLEMENTATION.
  METHOD zif_order_observer~on_order_created.
    mv_was_called = abap_true.
    ms_received   = is_order_data.
  ENDMETHOD.
ENDCLASS.

Inject the mock, trigger the action, assert mv_was_called = abap_true. Clean, isolated, fast.

Key Takeaways

  • The Factory Pattern centralizes object creation and makes your codebase extensible without scattering CREATE OBJECT logic everywhere.

  • The Observer Pattern decouples event producers from consumers, making it easy to add new reactions to system events without modifying existing code.

  • The Decorator Pattern lets you compose behaviors dynamically — far more flexible than deep inheritance hierarchies.

  • All three patterns make your code significantly easier to unit test, a non-negotiable requirement in modern SAP development.

  • These aren’t theoretical niceties — they solve concrete problems you face in every mid-to-large SAP project.

If you’re just getting started with OOP in ABAP and found this article dense, I’d recommend revisiting the Strategy Pattern from Part 1 of this series first — it lays the foundation that makes these patterns click.

In Part 3, we’ll explore the Command and Template Method patterns — both essential for building undo/redo mechanisms, batch processing pipelines, and workflow engines in SAP. Stay tuned.

What’s your experience with design patterns in ABAP? Have you applied any of these on a real project? I’d love to hear what worked, what didn’t, and what challenges you ran into. Drop a comment below or connect with me — let’s keep the conversation going.

PUBG: Blindspot macerası kısa sürdü

Geçen yıl büyük umutlarla oyun severlerin erişimine açılan PUBG: Blindspot oyunu beklentileri karşılayamadı. Zararın büyümesini önlemek adına geliştirici ekip oyunu iptal etme kararı aldı.

5 Open-Source AWS Security CLI Tools Worth Trying in 2026

TL;DR

In the context of security, even today, there’s a shortage of tools for everything. Prowler has a ton of checks. Trivy is the most well-known tool for containers and clouds. CloudFox is a tool for pentesters. Heimdall focuses on IAM privilege escalation. cloud-audit correlates findings, assembles them into a single attack chain, and provides fixes for implementation via Terraform or the CLI.

There’s something for everyone – it’s important to choose the right one for your work style.

The landscape

Have you ever wondered that in today’s technological age, a tool that could do everything for us would be useful? You know, literally everything. We’ll wake up in the morning and an automatically generated list will appear on our laptop, like, “Do this project today, use this AI agent, and then we’ll post it here and there – it will bring you success, fame, and money.” However, I now believe that even the most refined LLM model won’t replace creativity and real human needs.

Based on the above, I’ve concluded that security scanning in AWS isn’t as straightforward as it seems. Let’s answer the question together – do you know what you want to check and what to do with the results provided in the report?

There are tools that optimize the overview of our environment in terms of breadth – scanning 500+ rules across multiple clouds. Others, however, prepare information for depth optimization, searching in a smaller area but with much greater depth. Still others try to combine both horizons as optimally as possible. Is it possible to create a perfect tool that is free of noise and precisely meets the requirements of every administrator? In my opinion, no.

In this article, I’d like to present five CLI tools that I’ve personally tested, so I hope to provide an unbiased opinion on them (all as of April 2, 2026). If you want a deeper dive into how Prowler and ScoutSuite stack up against cloud-audit, I wrote a detailed comparison on my blog.

1. Prowler

Stars: over 13k | Checks: >550 (AWS) | Language: Python
GitHub: prowler-cloud/prowler
Install: pip install prowler

Anyone responsible for AWS environment security (and others) is likely familiar with Prowler. It’s by far the most popular open-source scanner. 572 AWS checks across 84 services and 41 compliance standards (CIS, SOC 2, HIPAA, PCI-DSS, NIST 800-53, and many more). If your auditor asks, “Are you using Prowler?” – that’s a sign that it’s popular.

Advantages:

  • The widest range of compliance among all OSS tools
  • Multi-cloud: AWS, Azure, GCP, Kubernetes, and others
  • Active development, large community, commercial support
  • HTML, CSV, JSON-OCSF, SARIF output

Where are the shortcomings:

  • Scan time: 10-30 minutes on a standard account (572 checks take time)
  • Attack path detection exists, but requires Prowler App (self-hosted Docker Compose + Neo4j + Cartography) or paid SaaS. The standard Prowler AWS CLI provides only simple results
  • Remediation is performed using text hints, not copy-and-paste commands
  • 572 findings can be cumbersome – you need to know which ones are relevant

Best for: Compliance-focused teams that need to check the box for CIS/SOC 2/HIPAA/PCI-DSS.

pip install prowler
prowler aws

2. Trivy

Stars: > 34k | AWS Checks: ~350-450 | Language: Go
GitHub: aquasecurity/trivy
Install: brew install trivy

This is an interesting resource. Trivy was initially designed for container vulnerability scanning, but later expanded to include cloud misconfiguration scanning. A key differentiator is the single binary that covers everything – container images, IaC files (Terraform, CloudFormation), Kubernetes, SBOM, licenses, and active AWS accounts.

What it does well:

  • A single binary covers containers + IaC + cloud + secrets + SBOM
  • Fast, Go-based
  • Huge community (34k stars)
  • CycloneDX and SPDX output for supply chain

Where it falls short:

  • AWS cloud scanning seems secondary to container scanning
  • No attack chain detection – no correlation between findings
  • Links to documentation pages for fixes, no CLI/Terraform output
  • AWS CIS compliance limited to versions 1.2 and 1.4 (not 3.0)
  • The March 2026 supply chain attack (trivy’s GitHub Action was compromised for about 12 hours) raised trust issues

Best for: Teams already using Trivy for containers and want a single tool for everything.

trivy aws --region eu-central-1

3. CloudFox

Stars: >2300 | Commands: 24 AWS enumeration modules | Language: Go
GitHub: BishopFox/cloudfox
Install: brew install cloudfox

Here we’re dealing with a slightly different type of tool. This isn’t a typical scanner, it’s a tool for cloud penetration testers. It’s a reconnaissance tool that enumerates what an attacker with given credentials can actually do – which roles to assume, which secrets to read, which instances to reach.

What it excels at:

  • An attacker’s perspective, not a defender’s checklist
  • Enumeration across accounts and services
  • Generates “loot files” – ready-to-use commands that an attacker could run
  • Good for red teams/penetration

Where it falls short:

  • No checks, no rules, no findings – just raw enumeration data
  • No suggestions for remediation or fixes
  • No compliance framework
  • No HTML/PDF reports – just table and CSV output
  • Requires manual analysis to connect facts to attack paths

Best for: Penetration testers and red teams assessing what can actually be accessed with permissions.

cloudfox aws --profile target-account all-checks

4. Heimdall

Stars: >140 | Patterns: >50 IAM escalations, >85 attack chains | Language: Python
GitHub: DenizParlak/heimdall
Install: from source (pip install -e .)

Heimdall primarily focuses on IAM privilege escalation. It checks whether a user with limited privileges could accidentally become an administrator. It maps trust relationships between IAM roles, policies, and services to find multi-hop escalation paths (A assumes B, B has a PassRole to C, C is an administrator).

What it does well:

  • Focuses on a difficult problem (privilege escalation) that most scanners miss
  • Over 85 attack chain patterns with MITRE ATT&CK mapping
  • Multi-hop detection (not just direct admin access)
  • Interactive terminal user interface
  • Ability to scan Terraform before deployment

Where it falls short:

  • Last commit: December 2025 (appears outdated)
  • No pip installation – cloning and installing from source required
  • Lack of compliance frameworks (CIS, SOC 2, etc.)
  • No remediation commands
  • Small community (146 stars, 4 commits)
  • AWS only

Best for: IAM-focused security reviews where the question “who can become an admin?” needs to be answered.

git clone https://github.com/DenizParlak/heimdall
cd heimdall && pip install -e .
heimdall scan

5. cloud-audit

Stars: >30 | Checks: 80 | Language: Python
GitHub: gebalamariusz/cloud-audit
Install: pip install cloud-audit
Website: haitmg.pl/cloud-audit

I created this tool. I tried to gather everything I needed most for my work. I used to conduct the same security reviews at AWS, but I was missing one tool that would truly streamline my work, hence the idea. I needed a scanner that would show how findings connect to actual attack paths, not just a flat list.

What it does well:

  • 20 attack chain rules that correlate findings (e.g., public SG + IMDSv1 + admin role = account takeover path)
  • Each finding includes AWS CLI + Terraform remediation code, not just descriptions
  • Compliance with AWS CIS v3.0 (62 checks) and SOC 2 Type II (43 criteria) with evidence for each check
  • Breach cost estimation per finding and attack chain (sources cited: IBM, Verizon DBIR)
  • Scan diff to track drift between runs
  • MCP server for AI agent integration (Claude, Cursor)
  • Under 60 seconds on a standard account

Where it falls short:

  • 80 checks compared to 572 in Prowler – smaller coverage
  • AWS only
  • Small community (31 stars)
  • Newer and less battle-tested
  • No multi-cloud

Best for: Teams that need fewer, high-signal findings with attack context and ready-to-paste fixes.

If you want to see it in action, here’s a 4-minute walkthrough on YouTube where I scan a real AWS account and find 3 attack chains.

pip install cloud-audit
cloud-audit scan -R

Side-by-side comparison

Prowler Trivy CloudFox Heimdall cloud-audit
AWS checks 572 ~400 24 commands 50+ patterns 80
Attack chains App only No No Yes (85+) Yes (20)
Remediation Text Doc links No No CLI + Terraform
Compliance 41 frameworks CIS 1.2/1.4 None MITRE only CIS v3.0, SOC 2
Multi-cloud Yes (12+) Yes Yes (3) No No
Scan time 10-30 min 2-5 min 1-3 min 1-2 min <60 sec
Output HTML, CSV, SARIF, JSON Table, SARIF, SPDX Table, CSV, JSON SARIF, CSV, JSON HTML, SARIF, JSON, MD
Cost estimation No No No No Yes

What I would actually use

For a compliance audit: Prowler. Nothing else comes close on framework coverage.

For a pentest: CloudFox. It thinks like an attacker.

For container + cloud in one pipeline: Trivy. Single binary, single CI step.

For a quick “what can an attacker actually do with my account”: cloud-audit or Heimdall. Depends on whether you want IAM escalation depth (Heimdall) or broader attack chains with fixes (cloud-audit).

There is no reason to pick just one. I run Prowler for compliance evidence and cloud-audit for the attack chain context and fix code. They complement each other.

If you’re looking for a more detailed breakdown of how these tools compare on specific AWS security checks, I covered that in my AWS Security Scanners Compared article. And if you’re setting up security scanning in CI/CD, check out the AWS Security Audit Checklist for a step-by-step approach.

Tools and star counts verified as of April 2026. Check each project’s GitHub for the latest.

The Fallback That Never Fires

Your agent hits a rate limit. The fallback logic kicks in, picks an alternative model. Everything should be fine.

Except the request still goes to the original model. And gets rate-limited again. And again. Forever.

The Setup

When your primary model returns 429:

  1. Fallback logic detects rate_limit_error
  2. Selects next model in the fallback chain
  3. Retries with the fallback model
  4. User never notices

OpenClaw has had model fallback chains for months, and they generally work well.

The Override

Issue #59213 exposes a subtle timing problem. Between steps 2 and 3, there is another system: session model reconciliation.

This reconciliation checks: the agent config says the model should be X. The session current model is Y. That is a mismatch. Let me fix it.

And it fixes the fallback selection right back to the rate-limited model.

The log tells the whole story:

[model-fallback/decision] next=kiro/claude-sonnet-4.6

[agent/embedded] live session model switch detected:
  kiro/claude-sonnet-4.6 -> anthropic/claude-sonnet-4-6

[agent/embedded] isError=true error=API rate limit reached.

Fallback selects → reconciliation overrides → 429 → repeat. Every 4-8 seconds, until someone manually runs /new.

Why This Happens

Two state management systems that do not know about each other:

  1. Fallback logic operates at the request level: for this attempt, use model X.
  2. Session reconciliation operates at the session level: this session should use model Y per config.

Neither communicates its intent. The reconciliation does not know a fallback is active. The fallback does not know reconciliation will override it.

This is the config-as-truth vs. runtime-as-truth tension. Config says use anthropic. Runtime says anthropic is rate-limited. Reconciliation trusts config. Runtime loses.

The Pattern: State Reconciliation Interference

Two subsystems that each behave correctly in isolation:

  • Fallback: correctly selects alternative model ✓
  • Reconciliation: correctly syncs session to config ✓

But composed together, they create a livelock. Each system passes its own tests. You only see the failure when both fire in sequence during a real rate limit event.

Three takeaways for agent builders:

  1. Runtime overrides need explicit priority over config reconciliation. If a subsystem intentionally diverges from config, that decision must be protected from being fixed.

  2. Test your failure paths end-to-end, not just unit-by-unit. Fallback + session management + rate limiting need to be tested as a composed system.

  3. Livelocks are worse than crashes. A crash you notice immediately. An infinite 429 loop looks like the agent is thinking for an uncomfortably long time.

The issue connects to the broader cluster of model selection bugs (#58533, #58556, #58539) reported recently. Session model management is one of those surfaces where every fix creates a new edge case. The real solution is probably a proper state machine with explicit transitions and priorities.

Follow me on X (@realwulong) for more AI agent reliability analysis.