<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jérôme GUYON</title>
    <description>The latest articles on DEV Community by Jérôme GUYON (@jerome_guyon_614ecd636c2c).</description>
    <link>https://dev.to/jerome_guyon_614ecd636c2c</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3880056%2Fb518d47b-aba1-41b0-b3c8-1e7d7762adc1.jpg</url>
      <title>DEV Community: Jérôme GUYON</title>
      <link>https://dev.to/jerome_guyon_614ecd636c2c</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jerome_guyon_614ecd636c2c"/>
    <language>en</language>
    <item>
      <title>Build your own AWS CLI service (yes, really)</title>
      <dc:creator>Jérôme GUYON</dc:creator>
      <pubDate>Tue, 23 Jun 2026 19:10:12 +0000</pubDate>
      <link>https://dev.to/aws-builders/build-your-own-aws-cli-service-yes-really-3g2g</link>
      <guid>https://dev.to/aws-builders/build-your-own-aws-cli-service-yes-really-3g2g</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fszvq4q8xlz0ujjqtp3pa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fszvq4q8xlz0ujjqtp3pa.png" alt=" " width="800" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I keep building internal tools and small utilities for my teams, and every single time, I face the same frustrating questions: if I ship a custom tool, I'm asking people to use something new, with a new set of credentials, and to read documentation they'll inevitably ignore.&lt;br&gt;&lt;br&gt;
That's a lot of possible friction for tools that are supposed to reduce friction 🙃.  &lt;/p&gt;

&lt;p&gt;Here's the thing that changed my approach: every developer or ops engineer I'm working with already spends their day in the AWS CLI.&lt;br&gt;&lt;br&gt;
They already have working credentials configured, they are fluent in &lt;code&gt;--profile&lt;/code&gt; for switching between accounts, &lt;code&gt;--query&lt;/code&gt; for JMESPath filtering, ...   &lt;/p&gt;

&lt;p&gt;What I could plug my internal APIs directly into the tool they already use every day?  &lt;/p&gt;

&lt;p&gt;Turns out botocore, the library underneath both the AWS CLI and boto3, is entirely model-driven : Every single AWS service is defined as a JSON file that describes its operations, request and response shapes, and botocore generates the client class at runtime.  &lt;/p&gt;

&lt;p&gt;And you can provide your own JSON model 💪 ! &lt;/p&gt;

&lt;p&gt;I built a working example to demonstrate the pattern from end to end that wraps the public API (the &lt;a href="https://builder.aws.com" rel="noopener noreferrer"&gt;AWS Community&lt;/a&gt; directory of Heroes and Community Builders) into what looks and feels like a native AWS CLI service.  &lt;/p&gt;

&lt;p&gt;The underlying technique applies to any REST API you want to expose.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws aws-community list-community-members &lt;span class="nt"&gt;--program&lt;/span&gt; HERO &lt;span class="nt"&gt;--category&lt;/span&gt; &lt;span class="s2"&gt;"Serverless Hero"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SigV4 authentication, standard CLI flags, table/JSON/YAML output, JMESPath queries, profile switching. All of this works with zero client code written on my side.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The pattern in four pieces.&lt;/strong&gt; The architecture looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AWS CLI / boto3  →  API Gateway (IAM auth)  →  Lambda  →  your backend  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;API Gateway&lt;/strong&gt; with &lt;code&gt;AWS_IAM&lt;/code&gt; authorization handles authentication for you. Every incoming request must carry a valid SigV4 signature, which means your team authenticates with the same IAM credentials they already use for every other AWS operation.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Lambda function&lt;/strong&gt; (or any API Gateway integration) receives the request body and does the actual work. In my demo it calls an external API, but yours could query DynamoDB, read from RDS, hit an internal microservice, or compute something on the fly.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A service model&lt;/strong&gt; (&lt;code&gt;service-2.json&lt;/code&gt;) describes your operations, their inputs, their outputs, and the data types involved. This is how botocore knows what CLI commands to expose, what parameters each command accepts, and how to serialize the request over the wire.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An endpoints file&lt;/strong&gt; (&lt;code&gt;endpoints.json&lt;/code&gt;) tells botocore which hostname to send requests to for each region. Without this file, users would have to pass &lt;code&gt;--endpoint-url&lt;/code&gt; manually on every invocation.
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Let me show you how to build one.&lt;/strong&gt;  &lt;/p&gt;

&lt;p&gt;Once you understand the structure, adapting it to your own API is mostly a matter of replacing the Lambda logic and renaming operations in the service model.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; Deploy the infrastructure. Pick whichever region you prefer. The project uses a single CloudFormation template with an inline Lambda for the demo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudformation deploy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--template-file&lt;/span&gt; cloudformation/template.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-name&lt;/span&gt; AWSCommunityService &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--capabilities&lt;/span&gt; CAPABILITY_IAM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; Retrieve the full API Gateway endpoint from the stack outputs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;API_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws cloudformation describe-stacks &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-name&lt;/span&gt; AWSCommunityService &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Stacks[0].Outputs[?OutputKey==`Endpoint`].OutputValue'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;  

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$API_ENDPOINT&lt;/span&gt;
&lt;span class="c"&gt;# https://a1b2c3d4e5.execute-api.eu-west-1.amazonaws.com/Prod  &lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3:&lt;/strong&gt; Inject the hostname into your endpoints file so botocore knows where to route requests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Extract just the hostname from the full URL  &lt;/span&gt;
&lt;span class="nv"&gt;API_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$API_ENDPOINT&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s|https://||'&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="nt"&gt;-f1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;  

&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt;.bak &lt;span class="s2"&gt;"s/REPLACE_WITH_ENDPOINT/&lt;/span&gt;&lt;span class="nv"&gt;$API_HOST&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt; models/endpoints.json &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm &lt;/span&gt;models/endpoints.json.bak  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4:&lt;/strong&gt; Set the &lt;code&gt;AWS_DATA_PATH&lt;/code&gt; environment variable to tell botocore where your model files live.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_DATA_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./models/  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 5:&lt;/strong&gt; Try it out.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;aws aws-community list-programs
&lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"Programs"&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt;
        &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;"ProgramName"&lt;/span&gt;: &lt;span class="s2"&gt;"COMMUNITY_BUILDER"&lt;/span&gt;,
            &lt;span class="s2"&gt;"Categories"&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"AI Engineering"&lt;/span&gt;, &lt;span class="s2"&gt;"Cloud Operations"&lt;/span&gt;, &lt;span class="s2"&gt;"Data"&lt;/span&gt;, ...]
        &lt;span class="o"&gt;}&lt;/span&gt;,
        &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;"ProgramName"&lt;/span&gt;: &lt;span class="s2"&gt;"HERO"&lt;/span&gt;,
            &lt;span class="s2"&gt;"Categories"&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"AI Hero"&lt;/span&gt;, &lt;span class="s2"&gt;"Community Hero"&lt;/span&gt;, &lt;span class="s2"&gt;"Container Hero"&lt;/span&gt;, ...]
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;aws aws-community list-community-members &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--program&lt;/span&gt; HERO &lt;span class="nt"&gt;--max-results&lt;/span&gt; 5 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output&lt;/span&gt; table &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'CommunityMembers[].{Name:Name,Category:Category,Country:Country}'&lt;/span&gt;

&lt;span class="nt"&gt;------------------------------------------------------------&lt;/span&gt;
|                   ListCommunityMembers                   |
+------------------+----------+----------------------------+
|     Category     | Country  |           Name             |
+------------------+----------+----------------------------+
|  Serverless Hero |  CA      |  Darryl Ruggles            |
|  AI Hero         |  AR      |  Ricardo Ceci              |
|  AI Hero         |  AR      |  Matias Kreder &lt;span class="o"&gt;[&lt;/span&gt;AWS Hero]  |
|  AI Hero         |  IT      |  Damiano Giorgi            |
|  Serverless Hero |  SE      |  Gunnar Grosch             |
+------------------+----------+----------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Et voilà! The &lt;code&gt;--output table&lt;/code&gt; formatting, the &lt;code&gt;--query&lt;/code&gt; JMESPath expression, the &lt;code&gt;--next-token&lt;/code&gt; pagination mechanism, they all work exactly as they would with any official AWS service.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It works in Python too.&lt;/strong&gt; The same model file that powers the CLI also gives you a fully functional boto3 client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;  

&lt;span class="n"&gt;acs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws-community&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;acs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list_community_members&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Program&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HERO&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MaxResults&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;member&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CommunityMembers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;  
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;member&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; - &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;member&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Category&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get input validation against the model, the Stubber for mocking in tests, and pagination support through &lt;code&gt;NextToken&lt;/code&gt;.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to write the service model.&lt;/strong&gt; This is the heart of the pattern. The &lt;code&gt;service-2.json&lt;/code&gt; file contains everything botocore needs to generate your client. Here's a minimal example showing a single operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"protocol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rest-json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"signingName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"execute-api"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"endpointPrefix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-service"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"operations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"GetThing"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"http"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"requestUri"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/Prod/GetThing"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"shape"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GetThingRequest"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"output"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"shape"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GetThingResponse"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shapes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"GetThingRequest"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"structure"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ThingId"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"members"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ThingId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"shape"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"String"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"GetThingResponse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"structure"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"members"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"shape"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"String"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"shape"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"String"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"String"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three metadata fields require your attention. The &lt;code&gt;protocol&lt;/code&gt; field should be &lt;code&gt;"rest-json"&lt;/code&gt; for a typical JSON API behind API Gateway. The &lt;code&gt;signingName&lt;/code&gt; must be &lt;code&gt;"execute-api"&lt;/code&gt; so that SigV4 signs the request with the correct service scope for API Gateway (change this only if you're running a custom authorizer). The &lt;code&gt;endpointPrefix&lt;/code&gt; is the name botocore uses both to look up the hostname in &lt;code&gt;endpoints.json&lt;/code&gt; and to determine the CLI command name, so &lt;code&gt;"my-service"&lt;/code&gt; means your users will type &lt;code&gt;aws my-service get-thing&lt;/code&gt;.  &lt;/p&gt;

&lt;p&gt;The shapes section defines your data structures using the same primitives that every official AWS service uses: &lt;code&gt;structure&lt;/code&gt; for objects, &lt;code&gt;list&lt;/code&gt; for arrays, &lt;code&gt;string&lt;/code&gt;, &lt;code&gt;integer&lt;/code&gt;, &lt;code&gt;boolean&lt;/code&gt;, and &lt;code&gt;timestamp&lt;/code&gt; for scalar types. You can add &lt;code&gt;min&lt;/code&gt;/&lt;code&gt;max&lt;/code&gt; constraints on integers, &lt;code&gt;pattern&lt;/code&gt; on strings, and mark fields as &lt;code&gt;required&lt;/code&gt;. If you want to see how a complex model looks, browse any service under &lt;code&gt;botocore/data/&lt;/code&gt; in the botocore GitHub repository, there are literally hundreds of production examples to learn from.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to write the endpoints file.&lt;/strong&gt; This tells botocore which hostname corresponds to your service in each region:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"partitions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"defaults"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hostname"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{service}.{region}.{dnsSuffix}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"protocols"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"https"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"signatureVersions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"v4"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dnsSuffix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"partition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-partition"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"partitionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"My Service"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"regionRegex"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^(us|eu|ap)&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;w+&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;d+$"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"regions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"eu-west-1"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EU (Ireland)"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"services"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"my-service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"endpoints"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"eu-west-1"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"hostname"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_API_ID.execute-api.eu-west-1.amazonaws.com"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;  
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you deploy your API Gateway in multiple regions, add an entry for each region under the &lt;code&gt;endpoints&lt;/code&gt; object. Each entry points to a different API Gateway deployment, giving you region-aware routing with no client-side logic.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;AWS_DATA_PATH&lt;/code&gt; instead of &lt;code&gt;aws configure add-model&lt;/code&gt;?&lt;/strong&gt; The &lt;code&gt;add-model&lt;/code&gt; command is tempting because it feels official, but it only copies &lt;code&gt;service-2.json&lt;/code&gt; to &lt;code&gt;~/.aws/models/&lt;/code&gt;. It completely ignores &lt;code&gt;endpoints.json&lt;/code&gt;, and botocore only reads custom endpoint definitions from directories listed in &lt;code&gt;AWS_DATA_PATH&lt;/code&gt;. So if you use &lt;code&gt;add-model&lt;/code&gt; alone, your users have to append &lt;code&gt;--endpoint-url https://xxxxx.execute-api.eu-west-1.amazonaws.com&lt;/code&gt; to every command.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Things to know:&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Access control through IAM.&lt;/strong&gt; Anyone calling your service needs &lt;code&gt;execute-api:Invoke&lt;/code&gt; permission on the API Gateway resource ARN. You manage access with the same IAM policies, roles, and permission boundaries you already use for every other AWS service in your organization.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No shell tab completion.&lt;/strong&gt; Custom services don't register themselves with the CLI's auto-completer. Your users can run &lt;code&gt;aws my-service help&lt;/code&gt; to discover available operations, but pressing Tab after &lt;code&gt;aws my-service&lt;/code&gt; won't suggest anything.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model format is technically undocumented.&lt;/strong&gt; AWS has never published a formal specification for &lt;code&gt;service-2.json&lt;/code&gt;. In practice, the format has remained stable for years and the hundreds of models bundled with botocore provide more than enough reference material to work from confidently.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pagination requires an extra file.&lt;/strong&gt; Botocore's built-in auto-paginator (the one behind &lt;code&gt;--page-size&lt;/code&gt; and automatic result aggregation) requires a separate &lt;code&gt;paginators-1.json&lt;/code&gt; model file. Without it, your users paginate manually by passing &lt;code&gt;--next-token&lt;/code&gt; from one call to the next.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Onboarding a new user takes 30 seconds.&lt;/strong&gt; Share the &lt;code&gt;models/&lt;/code&gt; folder (via git), have the user set one environment variable, and they're ready to go.
&lt;/li&gt;
&lt;li&gt;The full project is &lt;a href="https://github.com/guyon-it-consulting/build-your-own-awscli-service" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;— Jerome&lt;/p&gt;

</description>
      <category>awscli</category>
    </item>
    <item>
      <title>I built a browser-based chat UI for Kiro CLI and it complete how I use AI agents</title>
      <dc:creator>Jérôme GUYON</dc:creator>
      <pubDate>Thu, 21 May 2026 08:54:20 +0000</pubDate>
      <link>https://dev.to/aws-builders/i-built-a-browser-based-chat-ui-for-kiro-cli-and-it-complete-how-i-use-ai-agents-509</link>
      <guid>https://dev.to/aws-builders/i-built-a-browser-based-chat-ui-for-kiro-cli-and-it-complete-how-i-use-ai-agents-509</guid>
      <description>&lt;p&gt;I've been living inside &lt;a href="https://kiro.dev/cli/" rel="noopener noreferrer"&gt;Kiro CLI&lt;/a&gt; for months now. The &lt;a href="https://kiro.dev/docs/cli/acp/" rel="noopener noreferrer"&gt;ACP integration&lt;/a&gt; in my JetBrains IntelliJ IDEA is fantastic for coding sessions : I get the full agentic power of Kiro right where my code lives. And the new &lt;a href="https://kiro.dev/docs/cli/terminal-ui/" rel="noopener noreferrer"&gt;Terminal UI&lt;/a&gt; with its syntax-highlighted markdown, collapsible tool outputs, and crew monitor is genuinely impressive and I love it.&lt;/p&gt;

&lt;p&gt;But here's the thing. Not everything I do with an AI agent is &lt;em&gt;coding&lt;/em&gt;. Sometimes I'm researching an architecture pattern. Sometimes I'm exploring a hypothesis about a system design. Sometimes I'm doing deep analysis on a technical decision and I want to see tool calls, and streaming responses in a comfortable visual layout, the kind of experience you get on claude.ai or ChatGPT. A web chat, basically. Except one that talks to Kiro's full agentic capabilities instead of a vanilla LLM.&lt;/p&gt;

&lt;p&gt;So I built one with the &lt;a href="https://aws.amazon.com/vi/blogs/devops/ai-driven-development-life-cycle/" rel="noopener noreferrer"&gt;AI-Driven Development Lifecycle (AI-DLC)&lt;/a&gt; methodology and Kiro CLI as my usual coding partner, I created &lt;a href="https://github.com/guyon-it-consulting/kiro-ui" rel="noopener noreferrer"&gt;Kiro UI&lt;/a&gt; a self-hosted, browser-based chat interface that connects to Kiro CLI via the Agent Client Protocol.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Tv3TZUiRmpI"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works.&lt;/strong&gt; At its core, the architecture is simple: your browser (React 19 + Vite) connects via WebSocket to an Express server, which spawns and manages &lt;code&gt;kiro-cli acp&lt;/code&gt; processes over stdio. Each browser tab gets its own independent agent process with its own context. The communication uses the ACP protocol.&lt;/p&gt;

&lt;p&gt;What you get in the browser is a proper agentic chat experience: real-time streaming with markdown rendering and syntax highlighting, side-by-side diffs when the agent modifies files, collapsible tool call blocks with raw I/O inspection, an MCP server panel with live status indicators, permission management with auto-approve policies, slash command autocomplete, image and file attachments, a context usage meter showing how much of the context window you've consumed, ...&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Let me show you how to get started.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; Clone the repository and install dependencies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/guyon-it-consulting/kiro-ui.git
&lt;span class="nb"&gt;cd &lt;/span&gt;kiro-ui
npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; Make sure Kiro CLI is installed and authenticated. You need &lt;code&gt;kiro-cli&lt;/code&gt; in your PATH.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Verify kiro-cli is available&lt;/span&gt;
which kiro-cli
&lt;span class="c"&gt;# Should output something like: ~/.local/bin/kiro-cli&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3:&lt;/strong&gt;   A single command builds the frontend and starts the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm start
&lt;span class="c"&gt;# Open http://localhost:3000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Et voilà! You have a full-featured chat interface to Kiro's agentic capabilities running locally in your browser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4 (optional):&lt;/strong&gt; If you prefer a desktop app experience, Kiro UI packages as an Electron app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run electron:dev              &lt;span class="c"&gt;# Dev mode&lt;/span&gt;
npm run electron:build:mac        &lt;span class="c"&gt;# macOS → DMG + ZIP (universal)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Things to know:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosted and private&lt;/strong&gt; : Everything runs on localhost. No data leaves your machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-tab sessions&lt;/strong&gt; : Each tab spawns its own &lt;code&gt;kiro-cli acp&lt;/code&gt; process. They're fully independent : different contexts, different conversations, different working directories.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protocol-first design&lt;/strong&gt; : Every feature is driven by ACP. The UI adapts to what the agent supports, including Kiro-specific extensions (&lt;code&gt;_kiro.dev/*&lt;/code&gt;) for MCP servers, slash commands, and context compaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configuration&lt;/strong&gt; : Settings live in &lt;code&gt;~/.kiro-ui/&lt;/code&gt; with &lt;code&gt;settings.json&lt;/code&gt; for preferences and &lt;code&gt;trust.json&lt;/code&gt; for persistent tool permission rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License&lt;/strong&gt; : Apache 2.0. Use it, fork it, extend it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Give it a try: &lt;a href="https://github.com/guyon-it-consulting/kiro-ui" rel="noopener noreferrer"&gt;https://github.com/guyon-it-consulting/kiro-ui&lt;/a&gt;. I'd love to hear how you use it and what features you'd want next.&lt;/p&gt;

&lt;p&gt;— Jérôme&lt;/p&gt;

</description>
      <category>aws</category>
      <category>kiro</category>
      <category>agents</category>
      <category>showdev</category>
    </item>
    <item>
      <title>CloudShell - The Hidden API</title>
      <dc:creator>Jérôme GUYON</dc:creator>
      <pubDate>Mon, 11 May 2026 09:25:55 +0000</pubDate>
      <link>https://dev.to/aws-builders/cloudshell-the-hidden-api-51km</link>
      <guid>https://dev.to/aws-builders/cloudshell-the-hidden-api-51km</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Credits:&lt;/strong&gt; Inspired by Dan V.'s &lt;a href="https://github.com/dan-v/cloudshell-store" rel="noopener noreferrer"&gt;cloudshell-store&lt;/a&gt; project that demonstrated the CloudShell API use, I built an unofficial boto3 client by injecting a custom botocore service model and you get a native boto3 experience: SigV4 signing, retries...&lt;/p&gt;

&lt;p&gt;⚠️ Undocumented API — AWS can break it anytime. For exploration and learning only.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;🔗 &lt;a href="https://github.com/guyon-it-consulting/cloudshell-boto3" rel="noopener noreferrer"&gt;https://github.com/guyon-it-consulting/cloudshell-boto3&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  AWS CloudShell Has a Hidden API. Here's How to Use It with boto3.
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://aws.amazon.com/cloudshell/" rel="noopener noreferrer"&gt;AWS CloudShell&lt;/a&gt; is a free, browser-based shell built into the AWS Console — pre-authenticated, with the AWS CLI, Python, Docker, and ~1 GB of persistent storage per region. No setup, no EC2, no cost. It even supports VPC mode, so it can reach your private resources directly.&lt;/p&gt;

&lt;p&gt;But here's the thing: &lt;strong&gt;it has no public API&lt;/strong&gt;. No SDK, no CLI, no CloudFormation support. Console only.&lt;/p&gt;

&lt;p&gt;Or so it seems. 😁&lt;/p&gt;

&lt;h2&gt;
  
  
  Discovering the API
&lt;/h2&gt;

&lt;p&gt;If you open your browser's developer tools while launching CloudShell, you'll notice something interesting. The Console makes REST calls to &lt;code&gt;https://cloudshell.&amp;lt;region&amp;gt;.amazonaws.com&lt;/code&gt; — with endpoints like &lt;code&gt;/createEnvironment&lt;/code&gt;, &lt;code&gt;/describeEnvironments&lt;/code&gt;, &lt;code&gt;/createSession&lt;/code&gt;, and more. These requests are signed with standard AWS SigV4 authentication. They use the &lt;code&gt;cloudshell:&lt;/code&gt; IAM namespace. In other words, this is a full AWS API — it's just not documented.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/dan-v/cloudshell-store" rel="noopener noreferrer"&gt;Dan V.&lt;/a&gt; demonstrated that you could call these endpoints programmatically. Building on their work, I wanted to go further: what if we could use this API with the standard boto3 interface, complete with SigV4 signing, retries, pagination, and error handling?&lt;/p&gt;

&lt;p&gt;That's exactly what &lt;a href="https://github.com/guyon-it-consulting/cloudshell-boto3" rel="noopener noreferrer"&gt;cloudshell-boto3&lt;/a&gt; does.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;The trick is botocore's extensibility. Every AWS service in boto3 is described by a JSON service model — the same format AWS uses internally. If you provide your own model and inject it into botocore's loader, boto3 treats it as a standard service. You get a native client with proper request signing, serialization, and error handling.&lt;/p&gt;

&lt;p&gt;Here's how you create the client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;botocore.session&lt;/span&gt;

&lt;span class="c1"&gt;# Inject the custom service model
&lt;/span&gt;&lt;span class="n"&gt;bc_session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;botocore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;loader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bc_session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;data_loader&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;search_paths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;./my-additional-models&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Create the boto3 session and client
&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;botocore_session&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bc_session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cloudshell&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;eu-west-1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The API surface
&lt;/h2&gt;

&lt;p&gt;Once you have the client, you get access to the full CloudShell lifecycle. Let me walk you through the key operations.&lt;/p&gt;

&lt;h3&gt;
  
  
  List your environments
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe_environments&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Environments&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create an environment
&lt;/h3&gt;

&lt;p&gt;For a public environment, call with no arguments. For a VPC environment, pass the network configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Public environment
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_environment&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;env_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;EnvironmentId&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# VPC environment
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_environment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;EnvironmentName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;my-vpc-shell&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;VpcConfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;VpcId&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;vpc-0123456789abcdef0&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SubnetIds&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;subnet-0123456789abcdef0&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SecurityGroupIds&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sg-0123456789abcdef0&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;VPC environments require additional IAM permissions (&lt;code&gt;ec2:CreateNetworkInterface&lt;/code&gt;, &lt;code&gt;ec2:CreateTags&lt;/code&gt;, etc.). See the &lt;a href="https://docs.aws.amazon.com/cloudshell/latest/userguide/aws-cloudshell-vpc-permissions-1.html" rel="noopener noreferrer"&gt;AWS docs&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Wait for it, then connect
&lt;/h3&gt;

&lt;p&gt;Environments take a few seconds to start. Poll the status, then open an interactive session via SSM WebSocket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="c1"&gt;# Wait for RUNNING
&lt;/span&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_environment_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EnvironmentId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;env_id&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;RUNNING&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Open a session
&lt;/span&gt;&lt;span class="n"&gt;sess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;EnvironmentId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;env_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;SessionType&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;TMUX&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;TabId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
    &lt;span class="n"&gt;QCliDisabled&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sess&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SessionId&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sess&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;StreamUrl&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="c1"&gt;# wss://ssmmessages.&amp;lt;region&amp;gt;.amazonaws.com/...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use the &lt;code&gt;StreamUrl&lt;/code&gt;, &lt;code&gt;SessionId&lt;/code&gt;, and &lt;code&gt;TokenValue&lt;/code&gt; with the &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html" rel="noopener noreferrer"&gt;session-manager-plugin&lt;/a&gt; to get an interactive shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;

&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SessionId&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sess&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SessionId&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;TokenValue&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sess&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;TokenValue&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;StreamUrl&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sess&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;StreamUrl&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;session-manager-plugin&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;eu-west-1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;StartSession&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Upload and download files
&lt;/h3&gt;

&lt;p&gt;The API provides S3 presigned URLs for file transfer — the same mechanism the Console uses behind the scenes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_file_upload_urls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EnvironmentId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;env_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;script.sh&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;FileUploadPresignedUrl&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;FileUploadPresignedFields&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;script.sh&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Keep it alive
&lt;/h3&gt;

&lt;p&gt;CloudShell environments go to sleep after inactivity. Send heartbeats to prevent that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;heartbeat_loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;env_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_heart_beat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EnvironmentId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;env_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;heartbeat_loop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;env_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;daemon&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Lifecycle management
&lt;/h3&gt;

&lt;p&gt;You also get &lt;code&gt;start_environment&lt;/code&gt;, &lt;code&gt;stop_environment&lt;/code&gt;, &lt;code&gt;delete_environment&lt;/code&gt;, and &lt;code&gt;delete_session&lt;/code&gt; — the full lifecycle, from creation to cleanup.&lt;/p&gt;

&lt;h2&gt;
  
  
  The complete API reference
&lt;/h2&gt;

&lt;p&gt;Here's every operation available in the reverse-engineered service model:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;describe_environments&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List all CloudShell environments for the current IAM principal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;create_environment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create a new public or VPC environment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_environment_status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Get current status (&lt;code&gt;CREATING&lt;/code&gt;, &lt;code&gt;RUNNING&lt;/code&gt;, &lt;code&gt;SUSPENDED&lt;/code&gt;, ...)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;start_environment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Start a suspended environment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stop_environment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stop a running environment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;delete_environment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delete an environment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;create_session&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Open an interactive SSM WebSocket session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;delete_session&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Close an active session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;send_heart_beat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Keep an environment alive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_file_upload_urls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Get S3 presigned URLs for file upload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_file_download_urls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Get S3 presigned URLs for file download&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;put_credentials&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Forward console credentials (console-only, not usable programmatically)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  IAM permissions
&lt;/h2&gt;

&lt;p&gt;CloudShell uses the &lt;code&gt;cloudshell:&lt;/code&gt; IAM namespace. Each API operation maps to a corresponding IAM action (&lt;code&gt;cloudshell:CreateEnvironment&lt;/code&gt;, &lt;code&gt;cloudshell:DescribeEnvironments&lt;/code&gt;, etc.). For VPC environments, IAM also supports condition keys to restrict which VPCs, subnets, and security groups can be used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cloudshell:VpcIds&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cloudshell:SubnetIds&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cloudshell:SecurityGroupIds&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limits to keep in mind
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Max &lt;strong&gt;2 VPC environments&lt;/strong&gt; per IAM principal&lt;/li&gt;
&lt;li&gt;Max &lt;strong&gt;5 security groups&lt;/strong&gt; per VPC environment&lt;/li&gt;
&lt;li&gt;~&lt;strong&gt;1 GB&lt;/strong&gt; persistent storage per environment per region&lt;/li&gt;
&lt;li&gt;Environments &lt;strong&gt;sleep after inactivity&lt;/strong&gt; (use heartbeats to prevent)&lt;/li&gt;
&lt;li&gt;Environments can be &lt;strong&gt;reclaimed by AWS&lt;/strong&gt; at any time&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;Clone the repo and try it yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/guyon-it-consulting/cloudshell-boto3.git
&lt;span class="nb"&gt;cd &lt;/span&gt;cloudshell-boto3
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;span class="nv"&gt;AWS_PROFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-profile python simple_example.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repo includes a &lt;a href="https://github.com/guyon-it-consulting/cloudshell-boto3/blob/main/example.py" rel="noopener noreferrer"&gt;complete example&lt;/a&gt; that creates an environment, connects via &lt;code&gt;session-manager-plugin&lt;/code&gt;, injects credentials, and runs a command — the full lifecycle in one script.&lt;/p&gt;

&lt;p&gt;— Jérôme&lt;/p&gt;

</description>
      <category>api</category>
      <category>aws</category>
      <category>python</category>
      <category>showdev</category>
    </item>
    <item>
      <title>CloudWatch RUM vs. Ad blockers : How to fix possible missing telemetry</title>
      <dc:creator>Jérôme GUYON</dc:creator>
      <pubDate>Thu, 30 Apr 2026 16:56:50 +0000</pubDate>
      <link>https://dev.to/aws-builders/cloudwatch-rum-vs-ad-blockers-how-to-fix-possible-missing-telemetry-54j5</link>
      <guid>https://dev.to/aws-builders/cloudwatch-rum-vs-ad-blockers-how-to-fix-possible-missing-telemetry-54j5</guid>
      <description>&lt;p&gt;A few weeks ago, I was reviewing the &lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-RUM.html" rel="noopener noreferrer"&gt;Amazon CloudWatch RUM&lt;/a&gt; dashboard for a web application I maintain. Page views were suspiciously low. After some digging, I opened the browser's DevTools on my machine and there it was: uBlock Origin was quietly blocking every request to &lt;code&gt;dataplane.rum.eu-west-1.amazonaws.com&lt;/code&gt;. &lt;strong&gt;Our real user monitoring was blind to a non-negligible portion of our actual traffic.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;CloudWatch RUM is one of those AWS services that doesn't get the attention it deserves. But if you care about understanding how &lt;em&gt;real&lt;/em&gt; users experience your application — page load times, JavaScript errors, HTTP failures, Web Vitals — it's genuinely valuable. Here's what the dashboard looks like out of the box:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxytuf0selcjd0f1h6i5i.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxytuf0selcjd0f1h6i5i.gif" alt="RUM Dashboard" width="800" height="578"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The problem is that ad blockers treat its data plane endpoint the same way they treat any third-party tracking domain: a request flying off to &lt;code&gt;dataplane.rum.*.amazonaws.com&lt;/code&gt; looks exactly like telemetry that users might want to block.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The architecture fix is simple&lt;/strong&gt;: your CloudFront distribution already serves your frontend. Add one behavior — &lt;code&gt;/rum/*&lt;/code&gt; — that proxies to the RUM data plane. On the client side, point the &lt;a href="https://github.com/aws-observability/aws-rum-web" rel="noopener noreferrer"&gt;aws-rum-web&lt;/a&gt; SDK to &lt;code&gt;https://yourdomain.com/rum/&lt;/code&gt; instead of the default AWS endpoint. I use AWS CDK here, but the same works with CloudFormation, Terraform, or the console.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8tbii90og763tadbae6k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8tbii90og763tadbae6k.png" alt="RUM + Cloudfront architecture" width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Create the CloudWatch RUM app monitor and its Cognito identity pool.&lt;/strong&gt; RUM needs a Cognito identity pool with unauthenticated access to authorize browsers to send telemetry.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;cognito&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-cdk-lib/aws-cognito&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-cdk-lib/aws-iam&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;rum&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-cdk-lib/aws-rum&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Create an identity pool for RUM (unauthenticated access)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rumIdentityPool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;cognito&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CfnIdentityPool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RumIdentityPool&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;allowUnauthenticatedIdentities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Create the IAM role for unauthenticated users&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;guestRole&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Role&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RumGuestRole&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;assumedBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WebIdentityPrincipal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cognito-identity.amazonaws.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;StringEquals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cognito-identity.amazonaws.com:aud&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rumIdentityPool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ForAnyValue:StringLike&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cognito-identity.amazonaws.com:amr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unauthenticated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Attach the identity pool to the role&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;cognito&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CfnIdentityPoolRoleAttachment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RumRoleAttachment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;identityPoolId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rumIdentityPool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;unauthenticated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;guestRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roleArn&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Create the RUM app monitor&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rumAppMonitor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;rum&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CfnAppMonitor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RumAppMonitor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;myapp.example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;myapp-rum&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;appMonitorConfiguration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;allowCookies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// Allow X-Ray tracing&lt;/span&gt;
    &lt;span class="na"&gt;enableXRay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// Track 100% of sessions&lt;/span&gt;
    &lt;span class="na"&gt;sessionSampleRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;telemetries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;performance&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;errors&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;identityPoolId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rumIdentityPool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Grant the guest role permission to send RUM events&lt;/span&gt;
&lt;span class="nx"&gt;guestRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addToPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PolicyStatement&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rum:PutRumEvents&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;`arn:aws:rum:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:appmonitor/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;rumAppMonitor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Add the &lt;code&gt;/rum/*&lt;/code&gt; behavior to your CloudFront distribution.&lt;/strong&gt; This is the key part. I create an additional behavior that forwards requests matching &lt;code&gt;/rum/*&lt;/code&gt; to the RUM data plane origin.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;cf&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-cdk-lib/aws-cloudfront&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;origins&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-cdk-lib/aws-cloudfront-origins&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Build the additional behaviors map&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;additionalBehaviors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BehaviorOptions&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

&lt;span class="c1"&gt;// Proxy RUM traffic through CloudFront&lt;/span&gt;
&lt;span class="nx"&gt;additionalBehaviors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/rum/*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;origins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HttpOrigin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`dataplane.rum.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.amazonaws.com`&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;viewerProtocolPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ViewerProtocolPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HTTPS_ONLY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;cachePolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CachePolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CACHING_DISABLED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;allowedMethods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AllowedMethods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ALLOW_ALL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;originRequestPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OriginRequestPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ALL_VIEWER_EXCEPT_HOST_HEADER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Create the distribution (your existing one — just add the behavior)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;distribution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Distribution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Distribution&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;defaultBehavior&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;origins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;S3BucketOrigin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withOriginAccessControl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;websiteBucket&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;viewerProtocolPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ViewerProtocolPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REDIRECT_TO_HTTPS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;additionalBehaviors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;domainNames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;myapp.example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;certificate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;myCertificate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I choose &lt;code&gt;ALL_VIEWER_EXCEPT_HOST_HEADER&lt;/code&gt; because the RUM data plane expects the &lt;code&gt;Host&lt;/code&gt; header to match its own domain (&lt;code&gt;dataplane.rum.eu-west-1.amazonaws.com&lt;/code&gt;), not yours. If you forward the original &lt;code&gt;Host&lt;/code&gt;, the request will fail with a 403.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Point the RUM web client to your proxied endpoint.&lt;/strong&gt; Install the &lt;a href="https://www.npmjs.com/package/aws-rum-web" rel="noopener noreferrer"&gt;aws-rum-web&lt;/a&gt; package and configure the &lt;code&gt;endpoint&lt;/code&gt; to use your domain instead of the default AWS URL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install the RUM web client&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;aws-rum-web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AwsRum&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-rum-web&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rumClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AwsRum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your-app-monitor-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// from the CfnAppMonitor&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                     &lt;span class="c1"&gt;// your app version&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eu-west-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                 &lt;span class="c1"&gt;// region&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;sessionSampleRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;identityPoolId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eu-west-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// This is the magic line — point to your own domain&lt;/span&gt;
    &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://myapp.example.com/rum/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;telemetries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;performance&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;errors&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;allowCookies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;enableXRay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Et voilà! The browser now sends RUM telemetry to &lt;code&gt;https://myapp.example.com/rum/&lt;/code&gt;, which CloudFront proxies to the actual RUM data plane. Ad blockers see a first-party request and leave it alone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Things to know&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ad blocker filter lists&lt;/strong&gt; — Popular lists like EasyPrivacy and uBlock filters include patterns matching &lt;code&gt;dataplane.rum.*.amazonaws.com&lt;/code&gt; and the RUM CDN script URL (&lt;code&gt;client.rum.*.amazonaws.com&lt;/code&gt;). By proxying through your own domain, you bypass both. If you use the NPM installation method (recommended), the script itself is bundled in your app — only the data plane calls need proxying.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing&lt;/strong&gt; — $1 per 100,000 RUM events. A typical visit generates ~20 events. For 500K monthly visits: ~$100/month. CloudFront proxy overhead is negligible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session sample rate&lt;/strong&gt; — In production, consider setting &lt;code&gt;sessionSampleRate&lt;/code&gt; to something lower than &lt;code&gt;1&lt;/code&gt; (e.g., &lt;code&gt;0.1&lt;/code&gt; for 10% sampling) to control costs while still getting statistically meaningful data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;X-Ray integration&lt;/strong&gt; — With &lt;code&gt;enableXRay: true&lt;/code&gt;, RUM traces connect to your backend X-Ray traces, giving you end-to-end visibility from the browser click to the database query. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CloudWatch RUM is one of those "set it and forget it" services that quietly delivers real value — but only if it actually receives data. If you're already using it, proxy it through your own domain or you're likely missing a significant chunk of your user base. And if you're not using it yet, I'd strongly suggest you &lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-RUM.html" rel="noopener noreferrer"&gt;have a look&lt;/a&gt; — understanding how real users experience your app is worth the small setup effort.&lt;/p&gt;

&lt;p&gt;— Jerome&lt;/p&gt;

</description>
      <category>analytics</category>
      <category>aws</category>
      <category>monitoring</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Control Your Tesla from the Terminal with a Kiro CLI Skill</title>
      <dc:creator>Jérôme GUYON</dc:creator>
      <pubDate>Thu, 16 Apr 2026 08:49:16 +0000</pubDate>
      <link>https://dev.to/aws-builders/control-your-tesla-from-the-terminal-with-a-kiro-cli-skill-472g</link>
      <guid>https://dev.to/aws-builders/control-your-tesla-from-the-terminal-with-a-kiro-cli-skill-472g</guid>
      <description>&lt;p&gt;🔗 &lt;a href="https://github.com/guyon-it-consulting/myteslamate-skills-and-power" rel="noopener noreferrer"&gt;https://github.com/guyon-it-consulting/myteslamate-skills-and-power&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpba05j5jp5o47rim0kbg.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpba05j5jp5o47rim0kbg.gif" alt="Demo" width="708" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Last Tuesday, I was deep into a CDK refactor — the kind where I have 14 files open and I'm scared to blink. Then a thought hit me: did I turn on Sentry mode? My car was parked at the train station. I could grab my phone, open the Tesla app, wait for it to wake the car, scroll to Security, check the toggle… or I could just not break my flow.&lt;/p&gt;

&lt;p&gt;What if I could ask my coding assistant instead?&lt;/p&gt;

&lt;p&gt;Turns out, I can. I built a Kiro CLI skill that lets me control my Tesla straight from the terminal. &lt;strong&gt;It was also the perfect excuse to learn how to create a Kiro CLI Skill 😊&lt;/strong&gt; — and what better way to test a new feature than with something fun? Check the battery, lock the doors, toggle Sentry mode, pull up charging stats — all without leaving my editor. The secret ingredient? An MCP server that wraps APIs that any AI assistant can call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is MyTeslaMate?
&lt;/h2&gt;

&lt;p&gt;Before we get to the skill itself, let me introduce the engine behind it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://myteslamate.com" rel="noopener noreferrer"&gt;MyTeslaMate&lt;/a&gt; is the hosted version of &lt;a href="https://github.com/teslamate-org/teslamate" rel="noopener noreferrer"&gt;TeslaMate&lt;/a&gt;, the most popular open-source data logger for Tesla vehicles. If you own a Tesla and you haven't heard of TeslaMate, stop reading and go look at it. It continuously records every drive, charge session, sleep cycle, and software update into a PostgreSQL database, and exposes rich Grafana dashboards — battery degradation, charging curves, trip history, lifetime stats, vampire drain, efficiency trends, and more.&lt;/p&gt;

&lt;p&gt;MyTeslaMate takes all of that and hosts it for you. No Docker, no self-hosting, no database maintenance. It adds premium features like supercharger cost import, automations, fleet management, and — this is the part we care about — an MCP server.&lt;/p&gt;

&lt;p&gt;The MCP server at &lt;code&gt;https://mcp.myteslamate.com/mcp&lt;/code&gt; wraps two APIs into a single endpoint:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;th&gt;Tools&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tesla Fleet API&lt;/td&gt;
&lt;td&gt;98&lt;/td&gt;
&lt;td&gt;Vehicle commands, energy control, charging, navigation, security&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TeslaMate API&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Drive stats, charging analytics, efficiency data, trip history&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's &lt;strong&gt;100+ tools&lt;/strong&gt; accessible from any MCP-compatible AI assistant. Authentication is handled via OAuth SSO with Tesla's authorization server.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqwhc5uxby03ify43j94z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqwhc5uxby03ify43j94z.png" alt="MyTeslaMate dashboard" width="800" height="621"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a Kiro CLI Skill?
&lt;/h2&gt;

&lt;p&gt;A skill is how you teach Kiro CLI about a specific domain. It's a markdown file with YAML frontmatter that describes what the skill does and when to activate it. Think of it as a cheat sheet that Kiro loads on demand — it doesn't bloat your context until you actually need it.&lt;/p&gt;

&lt;p&gt;A skill has two parts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The frontmatter&lt;/strong&gt; — metadata that tells Kiro when to load the skill:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tesla-commands&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Control your Tesla vehicle and energy products via MyTeslaMate MCP server.&lt;/span&gt;
  &lt;span class="s"&gt;Use when the user asks about their car, vehicle status, lock/unlock, climate control, charging, Powerwall, solar production, energy optimization, Sentry mode, trip planning, drive statistics, or any Tesla-related query. Triggers on "tesla", "my car", "vehicle",&lt;/span&gt;
  &lt;span class="s"&gt;"charge", "battery", "climate", "powerwall", "solar", "sentry", "lock", "unlock",&lt;/span&gt;
  &lt;span class="s"&gt;"supercharger", "road trip", "energy", "charging history", "drive stats".&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The body&lt;/strong&gt; — capabilities, workflow instructions, and safety rules that guide the agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Tesla Commands&lt;/span&gt;

The &lt;span class="sb"&gt;`tesla-mcp`&lt;/span&gt; server exposes &lt;span class="gs"&gt;**100+ tools**&lt;/span&gt;.

&lt;span class="gu"&gt;## Capabilities&lt;/span&gt;
&lt;span class="gu"&gt;### Vehicle Control (64 commands)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Doors &amp;amp; Access: lock, unlock, open/close trunk, open frunk
&lt;span class="p"&gt;-&lt;/span&gt; Climate: start/stop HVAC, set temps, seat heaters, steering wheel heater
&lt;span class="p"&gt;-&lt;/span&gt; Charging: start/stop charge, set charge limit, schedule charging
...

&lt;span class="gu"&gt;## Workflow&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Use @tesla-mcp tools directly for all Tesla operations.
&lt;span class="p"&gt;2.&lt;/span&gt; Check current state before making changes.
&lt;span class="p"&gt;3.&lt;/span&gt; Wake the vehicle before sending action commands if the car is asleep.

&lt;span class="gu"&gt;## Safety&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Confirm with the user before executing security-sensitive commands.
&lt;span class="p"&gt;-&lt;/span&gt; Always show current state before making changes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The skill lives in &lt;code&gt;~/.kiro/skills/tesla-commands/SKILL.md&lt;/code&gt;. But a skill alone isn't enough — you also need an &lt;strong&gt;agent&lt;/strong&gt; that knows how to use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tesla agent configuration
&lt;/h2&gt;

&lt;p&gt;I created a dedicated agent: a JSON file that ties everything together: the skill, the MCP server and the tools.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tesla"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Tesla vehicle and energy control agent via MyTeslaMate MCP server"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"You help the user monitor and control their Tesla vehicle and energy products. Use the tesla-mcp tools for all operations. Present data in a human-readable format. Confirm before executing security-sensitive commands."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shell"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"grep"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"glob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@tesla-mcp"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"allowedTools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@tesla-mcp"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"skill://.kiro/skills/tesla-commands/SKILL.md"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tesla-mcp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://mcp.myteslamate.com/mcp"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"welcomeMessage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Tesla control ready. What would you like to do with your car?"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth noting here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;resources&lt;/code&gt; uses the &lt;code&gt;skill://&lt;/code&gt; URI scheme. This tells Kiro to load the skill's metadata at startup but defer loading the full content until it's actually needed. No wasted context. (See &lt;a href="https://kiro.dev/docs/cli/custom-agents/configuration-reference" rel="noopener noreferrer"&gt;Agent Configuration Reference&lt;/a&gt; in the Kiro docs: &lt;em&gt;"skill:// — Skills progressively loaded on demand"&lt;/em&gt;.)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mcpServers&lt;/code&gt; points to the remote MyTeslaMate MCP server in this case, no local mcp server needed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;allowedTools&lt;/code&gt; auto-approves &lt;code&gt;read&lt;/code&gt; and all &lt;code&gt;@tesla-mcp&lt;/code&gt; tools so you don't get prompted for every single API call.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to set it up
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; You'll need a Tesla vehicle (obviously) and a &lt;a href="https://myteslamate.com" rel="noopener noreferrer"&gt;MyTeslaMate&lt;/a&gt; account. Sign up, link your Tesla account, and pick a subscription plan. This gives you access to the MCP server and the TeslaMate analytics dashboards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; Clone the repo and copy the files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/guyon-it-consulting/myteslamate-skills-and-power.git
&lt;span class="nb"&gt;cd &lt;/span&gt;myteslamate-skills-and-power

&lt;span class="c"&gt;# Copy the skill&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; kiro/skills/tesla-commands ~/.kiro/skills/

&lt;span class="c"&gt;# Copy the agent&lt;/span&gt;
&lt;span class="nb"&gt;cp &lt;/span&gt;kiro/agents/tesla.json ~/.kiro/agents/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; Start a new Kiro CLI session and switch to Tesla Agent&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kiro-cli
&lt;span class="go"&gt;
/agent swap tesla

✔ Switched to agent: tesla
Tesla control ready. What would you like to do with your car?
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3:&lt;/strong&gt; Authenticate with Tesla.&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;/mcp&lt;/code&gt;. The &lt;code&gt;tesla-mcp&lt;/code&gt; server will show &lt;code&gt;⚠ auth-required&lt;/code&gt;. Follow the instructions to copy the OAuth URL, open it in your browser, and sign in with your Tesla account. This is a one-time step — the token persists across sessions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiobcml9paboucvq4yr6m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiobcml9paboucvq4yr6m.png" alt="OAuth flow" width="800" height="553"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Using it
&lt;/h2&gt;

&lt;p&gt;Now the fun part. Just talk naturally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; Is Sentry mode on?

Nope — Sentry Mode is currently off on your Model Y. 
It is available though, so I can turn it on for you if you'd like.
Want me to enable it?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; What's my battery at?

Fresh data from TeslaMate:
- Battery level: 80% (hit your charge limit!)
- Estimated range: ~411 km
- Charging state: Complete — plugged in but done charging
- Energy added this session: 34.1 kWh

Looks like it finished charging since we last checked (was at 77% earlier).
You're topped up to your 80% limit.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; Lock my car

Your car is already locked according to the latest status.
No action needed!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; How much did I spend on charging this month?

Here's your April charging summary (so far, through April 10):

┌──────────────────┬──────────┬───────────────┬────────────┐
│ Type             │ Sessions │ Energy        │ Cost       │
├──────────────────┼──────────┼───────────────┼────────────┤
│ Supercharger     │ 2        │ 105.0 kWh     │ €37.88     │
│ Home/Destination │ 5        │ 60.98 kWh     │ €0*        │
│ **Total**        │ **7**    │ **166.0 kWh** │ **€37.88** │
└──────────────────┴──────────┴───────────────┴────────────┘

No Data, estimating home/destination charging at ~€0.25/kWh, that's roughly €15 extra,
bringing the estimated total to around €53
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; What's my battery health status ?

Your battery is in great shape:
- Battery health: 98.6%
- Current capacity: 75.7 kWh (out of 76.8 kWh max)
- Current max range: 506 km (vs 512 km when new)
- Degradation: only ~1.4% 

Very healthy for a car with 435 drives logged.
Nothing to worry about there.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent checks the current state before making changes, asks for confirmation on security-sensitive commands, and presents data with proper units. All of that behavior comes from the SKILL.md instructions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things to know
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;100+ tools, one endpoint.&lt;/strong&gt; The MyTeslaMate MCP server covers vehicle control, vehicle data, energy/Powerwall/solar, charging history, and TeslaMate analytics. You don't need to know which API to call — the agent figures it out.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Wake before you command.&lt;/strong&gt; Tesla vehicles go to sleep to save battery. The skill instructs the agent to wake the car before sending action commands. You'll see a brief delay the first time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Safety checks.&lt;/strong&gt; The skill explicitly tells the agent to confirm before executing unlock, disable Sentry, remote start, or erase data. You won't accidentally unlock your car because of a typo.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;OAuth, not API keys.&lt;/strong&gt; Authentication goes through Tesla's OAuth flow via MyTeslaMate. The token is scoped and can be revoked from your Tesla account at any time.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;— Jérôme&lt;/p&gt;

</description>
      <category>kiro</category>
      <category>aws</category>
      <category>tesla</category>
      <category>myteslamate</category>
    </item>
  </channel>
</rss>
