<?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: Morgan Seznec</title>
    <description>The latest articles on DEV Community by Morgan Seznec (@morganseznec).</description>
    <link>https://dev.to/morganseznec</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%2F4219038%2Fce591e29-85c3-4a14-ae3d-26fa399131e2.png</url>
      <title>DEV Community: Morgan Seznec</title>
      <link>https://dev.to/morganseznec</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/morganseznec"/>
    <language>en</language>
    <item>
      <title>hop: reach any EC2 instance in any AWS account with one command</title>
      <dc:creator>Morgan Seznec</dc:creator>
      <pubDate>Sat, 31 Oct 2026 13:07:27 +0000</pubDate>
      <link>https://dev.to/morganseznec/how-i-stopped-copying-ec2-instance-ids-and-why-my-cli-refuses-to-guess-2of3</link>
      <guid>https://dev.to/morganseznec/how-i-stopped-copying-ec2-instance-ids-and-why-my-cli-refuses-to-guess-2of3</guid>
      <description>&lt;p&gt;I work across a lot of AWS accounts. Reaching one machine used to take several steps: remember which account it lives in, find the matching profile in &lt;code&gt;~/.aws/config&lt;/code&gt;, check the region, look up the instance ID in the console or with &lt;code&gt;aws ec2 describe-instances&lt;/code&gt;, and finally type:&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="go"&gt;aws ssm start-session --target i-0ee8781d10780ec32 --profile acme-prod --region eu-west-3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few times a day, over fifteen accounts and three regions, that adds up. I wrote some shell functions to shorten it. They kept growing, and they became hop.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fmorganseznec%2Fhop%2Fmain%2Fdocs%2Fdemo.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fmorganseznec%2Fhop%2Fmain%2Fdocs%2Fdemo.gif" alt="hop listing a fictional fleet, choosing between two matches, then connecting by IP" width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Searching every account at once
&lt;/h2&gt;

&lt;p&gt;hop reads every profile in &lt;code&gt;~/.aws/config&lt;/code&gt; and &lt;code&gt;~/.aws/credentials&lt;/code&gt; and queries each one in its own region. The queries run in parallel, 16 at a time, and the result stays in a local cache for 15 minutes. A fleet of a few dozen accounts comes back in a few seconds the first time, and instantly after that.&lt;/p&gt;

&lt;p&gt;Then you type whatever you remember about the machine:&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;hop shop app        #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;words of its name
&lt;span class="gp"&gt;hop 10.90.2.73      #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;a private IP, copied from an alert
&lt;span class="gp"&gt;hop i-0ee8781d...   #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;an instance ID
&lt;span class="gp"&gt;hop                 #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;a fuzzy picker over the whole fleet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;hop ls&lt;/code&gt; shows what it found, with the profile and region of each instance:&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;hop &lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;span class="go"&gt;  NAME          PROFILE           REGION          ENV   IP           AGENT   LOGGING
  shop-app      acme-prod         eu-west-3       prod  10.83.2.182  online  yes
  grafana       globex-prod       us-east-1       prod  10.90.2.73   online  yes
  tps-api       initech-test      ap-southeast-2  test  10.68.3.138  online  yes
&lt;/span&gt;&lt;span class="c"&gt;  ...
&lt;/span&gt;&lt;span class="go"&gt;26 instances, 9 profiles, regions eu-west-3, eu-west-1, us-east-1, us-west-2, eu-central-1, ap-southeast-2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shell goes through Session Manager. The instance needs the SSM agent and an instance role; port 22 can stay closed, and there are no SSH keys to hand out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it refuses to guess
&lt;/h2&gt;

&lt;p&gt;A tool that opens shells on production servers should not pick a machine on its own when the query is vague, and most of the design work went there.&lt;/p&gt;

&lt;p&gt;hop connects without asking only when exactly one instance matches strongly: its exact name, ID or IP, or whole words of a single name. Everything looser goes through you.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When two instances share the words, like &lt;code&gt;shop-app&lt;/code&gt; in production and &lt;code&gt;shop-app-staging&lt;/code&gt;, hop opens the picker with both.&lt;/li&gt;
&lt;li&gt;A typo such as &lt;code&gt;shop-ap&lt;/code&gt; makes hop ask whether you meant &lt;code&gt;shop-app&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A private IP has to match exactly, so &lt;code&gt;10.0.1.2&lt;/code&gt; never lands on &lt;code&gt;10.0.1.21&lt;/code&gt;. The same private IP can also exist in two accounts, so &lt;code&gt;hop run&lt;/code&gt; and &lt;code&gt;hop push&lt;/code&gt; ask before acting on an IP when several profiles are in scope.&lt;/li&gt;
&lt;li&gt;Without a terminal (a script, a cron job, ssh's ProxyCommand), hop has nobody to ask. It prints the candidates and stops:
&lt;/li&gt;
&lt;/ul&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;hop run &lt;span class="s2"&gt;"shop app prod"&lt;/span&gt; &lt;span class="nb"&gt;uptime&lt;/span&gt; &amp;lt; /dev/null
&lt;span class="go"&gt;hop: "shop app prod" only partly matches shop-app (acme-prod, eu-west-3): give its full name, ID or IP, or narrow with -p, -r or -e (no terminal to confirm)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cache brings its own risk. An instance can be replaced between two refreshes, and its old name or IP can then belong to another machine. So when the data for an instance is more than a minute old, hop looks it up again in its account before acting. If it is gone, stopped, renamed or has a new IP, hop says what changed and searches again:&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="go"&gt;hop: shop-app (i-0ee8781d10780ec32, acme-prod, eu-west-3) is now stopped: searching again
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Knowing whether the session is recorded
&lt;/h2&gt;

&lt;p&gt;Session Manager can record every shell to CloudWatch Logs or S3. The setting lives in the &lt;code&gt;SSM-SessionManagerRunShell&lt;/code&gt; document, once per account and per region, and it often differs from one account to the next. hop reads it along with the inventory and tells you before the shell opens:&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;hop shop app
&lt;span class="go"&gt;hop: session LOGGED on shop-app (i-0ee8781d10780ec32, acme-prod, eu-west-3) to /ssm-logs. Everything that follows is recorded.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On an account where no destination is set, hop warns and asks &lt;code&gt;Open anyway? [y/N]&lt;/code&gt;. With &lt;code&gt;transcript: require&lt;/code&gt; in its configuration, it refuses those sessions.&lt;/p&gt;

&lt;h2&gt;
  
  
  SSO across many accounts
&lt;/h2&gt;

&lt;p&gt;With IAM Identity Center, dozens of profiles often share one portal and one token. hop groups the profiles by portal, so one &lt;code&gt;hop login&lt;/code&gt; signs in to every account behind it. It also checks the token before connecting, and when it has expired, it offers to open the portal right away.&lt;/p&gt;

&lt;p&gt;A profile can be defined twice, with SSO in &lt;code&gt;config&lt;/code&gt; and with keys in &lt;code&gt;credentials&lt;/code&gt;. The AWS CLI then uses SSO, while the Go SDK on its own would pick the keys. hop does what the CLI does. &lt;code&gt;hop doctor&lt;/code&gt; lists these profiles, along with expired tokens, long-lived keys and profiles that need an MFA code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other commands
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;hop run tools uptime&lt;/code&gt; runs a command and returns its exit code. If you stop waiting, hop cancels a command the instance has not picked up yet, so it does not run later behind your back.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hop tunnel tools 5432 15432 --host db.internal&lt;/code&gt; forwards a port, here to a database that only the instance can reach.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hop push ./app.conf tools /etc/app.conf&lt;/code&gt; copies a small file and compares the SHA-256 on both sides.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hop init ssh &amp;gt;&amp;gt; ~/.ssh/config&lt;/code&gt; sends &lt;code&gt;ssh ec2-user@tools.hop&lt;/code&gt;, &lt;code&gt;scp&lt;/code&gt; and &lt;code&gt;rsync&lt;/code&gt; through Session Manager as well.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GCP support is experimental: with &lt;code&gt;gcp_projects&lt;/code&gt; in the configuration, hop lists Compute Engine instances across your projects and opens shells on them through IAP. It only does shells there for now. If you would use it, or want Azure, tell me in &lt;a href="https://github.com/morganseznec/hop/discussions/19" rel="noopener noreferrer"&gt;this discussion&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A release you can check
&lt;/h2&gt;

&lt;p&gt;hop opens shells on your servers, so what you install should be verifiable. Each release has its checksums signed with Sigstore from the GitHub Actions run, SLSA level 3 provenance, and GitHub attestations for the archives and the Docker image:&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="go"&gt;gh attestation verify hop_0.3.1_linux_amd64.tar.gz -R morganseznec/hop
slsa-verifier verify-artifact --provenance-path hop.intoto.jsonl \
  --source-uri github.com/morganseznec/hop --source-tag v0.3.1 hop_0.3.1_linux_amd64.tar.gz
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Trying it
&lt;/h2&gt;

&lt;p&gt;On macOS:&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="go"&gt;brew install --cask morganseznec/tap/hop-cli
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Linux packages (deb, rpm, apk, Arch), Windows archives and a Docker image are on the &lt;a href="https://github.com/morganseznec/hop/releases" rel="noopener noreferrer"&gt;releases page&lt;/a&gt;, and &lt;code&gt;go install github.com/morganseznec/hop/cmd/hop@latest&lt;/code&gt; works too. The README lists the IAM permissions hop needs.&lt;/p&gt;

&lt;p&gt;The code is on GitHub under the MIT license: &lt;a href="https://github.com/morganseznec/hop" rel="noopener noreferrer"&gt;https://github.com/morganseznec/hop&lt;/a&gt;. I would like to hear from people who manage many accounts, especially about what is missing for your setup.&lt;/p&gt;

&lt;p&gt;Built with Claude Code. This article was written with Claude.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
