<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Testing on Brett's Blog</title><link>/tags/testing/</link><description>Recent content in Testing on Brett's Blog</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><managingEditor>brett@network-notes.com (Brett Lykins)</managingEditor><webMaster>brett@network-notes.com (Brett Lykins)</webMaster><copyright>© 2015-2026 Brett Lykins</copyright><lastBuildDate>Fri, 17 Apr 2026 19:00:00 -0500</lastBuildDate><atom:link href="/tags/testing/feed.xml" rel="self" type="application/rss+xml"/><item><title>CiSSHGo: A Lightweight SSH Device Emulator for Network Automation Testing</title><link>/posts/2026/cisshgo-ssh-device-emulator/</link><pubDate>Fri, 17 Apr 2026 19:00:00 -0500</pubDate><author>brett@network-notes.com (Brett Lykins)</author><guid>/posts/2026/cisshgo-ssh-device-emulator/</guid><description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure&lt;/strong&gt;: I&amp;rsquo;m a contributor to CiSSHGo and use it in my own projects. I&amp;rsquo;ll be straightforward about what it does well and where it falls short.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;You need to test your network automation code. Maybe it&amp;rsquo;s an Ansible playbook that configures interfaces across a thousand switches. Maybe it&amp;rsquo;s a Nornir script that collects &lt;code&gt;show version&lt;/code&gt; from every device in your inventory. Maybe it&amp;rsquo;s a REST API that proxies SSH commands to network equipment.&lt;/p&gt;
&lt;p&gt;The standard answer is &amp;ldquo;spin up a lab.&amp;rdquo; CML, EVE-NG, GNS3, ContainerLab — all solid tools that run actual NOS images. They give you real device behavior, real protocol interactions, real forwarding planes. They&amp;rsquo;re also heavy. A single Cisco CSR1000v image wants 4GB of RAM. Multiply that by the number of devices in your test topology and you&amp;rsquo;re looking at serious compute just to validate that your automation sends the right commands and parses the output correctly.&lt;/p&gt;
&lt;p&gt;For a lot of automation testing, you don&amp;rsquo;t need a real forwarding plane. You need something that accepts an SSH connection, presents a prompt, and responds to &lt;code&gt;show version&lt;/code&gt; with the right output. That&amp;rsquo;s the problem &lt;a href="https://github.com/tbotnz/cisshgo"&gt;CiSSHGo&lt;/a&gt; solves.&lt;/p&gt;
&lt;h2 id="what-cisshgo-is-and-isnt"&gt;What CiSSHGo Is (and Isn&amp;rsquo;t)&lt;/h2&gt;
&lt;p&gt;CiSSHGo is a single Go binary that spawns SSH listeners, each emulating a network device by playing back pre-defined command transcripts. You define what commands a device supports and what output it returns, and CiSSHGo handles the SSH session, prompt rendering, command matching, and context transitions (&lt;code&gt;&amp;gt;&lt;/code&gt; → &lt;code&gt;#&lt;/code&gt; → &lt;code&gt;(config)#&lt;/code&gt; → &lt;code&gt;(config-if)#&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;It is not a network simulator. There&amp;rsquo;s no forwarding plane, no routing protocol adjacencies, no MAC address table. It doesn&amp;rsquo;t run IOS or EOS or Junos. It plays back text files. That constraint is also its strength: a single CiSSHGo process can spawn thousands of SSH listeners using Go&amp;rsquo;s goroutine-per-listener model, each consuming a few kilobytes of memory. Try that with a Python-based mock server or a fleet of virtual routers.&lt;/p&gt;
&lt;p&gt;CiSSHGo isn&amp;rsquo;t new. Tony Nealon (&lt;a href="https://github.com/tbotnz"&gt;tbotnz&lt;/a&gt;, also the creator of &lt;a href="https://github.com/tbotnz/netpalm"&gt;netpalm&lt;/a&gt;) first published it in August 2020, and it&amp;rsquo;s been in active use since. What is new is the scope of recent work: the project went from a single-platform proof of concept to a &lt;a href="https://github.com/tbotnz/cisshgo/releases/tag/v1.0.0"&gt;v1.0.0 stable release&lt;/a&gt; in March 2026 with multi-vendor support, an inventory system, scenario mode, and proper release engineering. That&amp;rsquo;s enough of a capability jump to warrant a re-introduction for anyone who hasn&amp;rsquo;t looked at it recently. It&amp;rsquo;s MIT licensed, ships as pre-built binaries for Linux/macOS/Windows (amd64 and arm64), and publishes multi-arch Docker images to &lt;a href="https://github.com/tbotnz/cisshgo/pkgs/container/cisshgo"&gt;GitHub Container Registry&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="capabilities"&gt;Capabilities&lt;/h2&gt;
&lt;h3 id="seven-platforms-out-of-the-box"&gt;Seven Platforms Out of the Box&lt;/h3&gt;
&lt;p&gt;CiSSHGo ships with transcript libraries for seven platforms: Cisco CSR1000v, IOS, IOS-XR, ASA, NX-OS, Arista EOS, and Juniper Junos. Each platform comes with &lt;code&gt;show version&lt;/code&gt;, &lt;code&gt;show ip interface brief&lt;/code&gt; (or equivalent), and &lt;code&gt;show running-config&lt;/code&gt; transcripts sourced from &lt;a href="https://github.com/networktocode/ntc-templates"&gt;NTC Templates&lt;/a&gt; test fixtures.&lt;/p&gt;
&lt;p&gt;The transcript map is a YAML file that defines everything about a platform — hostname, credentials, supported commands, context hierarchy, and prompt format:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;platforms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;csr1000v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;vendor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;cisco&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&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;&amp;#34;cisshgo1000v&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;admin&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;admin&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;command_transcripts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;show version&amp;#34;: &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;cisco/csr1000v/show_version.txt&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;show ip interface brief&amp;#34;: &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;cisco/csr1000v/show_ip_interface_brief.txt&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;show running-config&amp;#34;: &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;cisco/csr1000v/show_running-config.txt&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;terminal length 0&amp;#34;: &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;generic_empty_return.txt&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;context_hierarchy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;(config-if)#&amp;#34;: &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;(config)#&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;(config)#&amp;#34;: &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;#&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;#&amp;#34;: &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;gt;&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;&amp;gt;&amp;#34;: &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;exit&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;context_search&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;interface&amp;#34;: &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;(config-if)#&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;configure terminal&amp;#34;: &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;(config)#&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;enable&amp;#34;: &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;#&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;base&amp;#34;: &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;gt;&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Adding a new platform or new commands is just adding text files and YAML entries. No Go code required.&lt;/p&gt;
&lt;h3 id="inventory-mode"&gt;Inventory Mode&lt;/h3&gt;
&lt;p&gt;For testing against multiple device types, CiSSHGo supports an inventory file that spawns different platforms on different ports from a single process:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;csr1000v&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ios&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;nxos&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;eos&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;junos&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;One binary, 50 SSH listeners, five different platform personalities. Each gets its own hostname, credentials, prompt style, and command set.&lt;/p&gt;
&lt;p&gt;&lt;img src="../../img/2026/cisshgo-inventory-listeners.svg" alt="Inventory mode: mixed platforms and scenarios from a single binary"&gt;&lt;/p&gt;
&lt;h3 id="scenario-mode"&gt;Scenario Mode&lt;/h3&gt;
&lt;p&gt;This is the feature that moved CiSSHGo from &amp;ldquo;useful for smoke tests&amp;rdquo; to &amp;ldquo;useful for real integration testing.&amp;rdquo; A scenario defines an ordered sequence of commands with different outputs at each step. The classic use case: &lt;code&gt;show running-config&lt;/code&gt; returns one output before a configuration change and different output after.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;scenarios&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;csr1000v-add-interface&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;csr1000v&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;enable&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;generic_empty_return.txt&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;show running-config&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;scenarios/csr1000v-add-interface/running_config_before.txt&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;configure terminal&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;generic_empty_return.txt&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;interface GigabitEthernet0/0/2&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;generic_empty_return.txt&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;ip address 172.16.0.1 255.255.255.0&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;generic_empty_return.txt&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;no shutdown&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;generic_empty_return.txt&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;end&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;generic_empty_return.txt&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;show running-config&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;scenarios/csr1000v-add-interface/running_config_after.txt&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Each SSH session gets its own sequence pointer. Commands that don&amp;rsquo;t match the current step fall through to the platform&amp;rsquo;s normal command set. Scenario mode also handles interface abbreviation matching — &lt;code&gt;int g0/0/2&lt;/code&gt; matches &lt;code&gt;interface GigabitEthernet0/0/2&lt;/code&gt; without a hardcoded abbreviation table. The sequence step itself provides the ground truth.&lt;/p&gt;
&lt;h3 id="flexible-prompts"&gt;Flexible Prompts&lt;/h3&gt;
&lt;p&gt;Not every NOS uses the Cisco &lt;code&gt;hostname#&lt;/code&gt; prompt format. CiSSHGo supports a &lt;code&gt;prompt_format&lt;/code&gt; field for platforms like Junos that use &lt;code&gt;user@hostname&amp;gt;&lt;/code&gt; style prompts, and &lt;code&gt;context_prefix_lines&lt;/code&gt; for multi-line prompts like Junos&amp;rsquo;s &lt;code&gt;[edit]&lt;/code&gt; prefix:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;junos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;vendor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;juniper&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&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;&amp;#34;cisshgo-junos&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;admin&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;prompt_format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;{username}@{hostname}{context}&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;context_prefix_lines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;#&amp;#34;: &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;[edit]&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;This produces:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;admin@cisshgo-junos&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;admin@cisshgo-junos&amp;gt; configure
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[edit]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;admin@cisshgo-junos#
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h3 id="session-behavior"&gt;Session Behavior&lt;/h3&gt;
&lt;p&gt;Once a listener is running, CiSSHGo handles the session the way you&amp;rsquo;d expect from a real device. Commands are matched by prefix — &lt;code&gt;sh ver&lt;/code&gt; resolves to &lt;code&gt;show version&lt;/code&gt;, &lt;code&gt;sh ip int br&lt;/code&gt; to &lt;code&gt;show ip interface brief&lt;/code&gt; — and ambiguous abbreviations get the familiar &lt;code&gt;% Ambiguous command&lt;/code&gt; error. Your automation code can use the same shortened commands it uses against real hardware.&lt;/p&gt;
&lt;p&gt;Both interactive shell sessions and exec mode (&lt;code&gt;ssh host &amp;quot;show version&amp;quot;&lt;/code&gt;) work. Automation tools like Ansible&amp;rsquo;s &lt;code&gt;network_cli&lt;/code&gt; connection plugin use exec mode for command execution, so this matters for realistic testing.&lt;/p&gt;
&lt;p&gt;Transcript files support Go&amp;rsquo;s &lt;a href="https://pkg.go.dev/text/template"&gt;text/template&lt;/a&gt; syntax. You can reference device fields like &lt;code&gt;{{.Hostname}}&lt;/code&gt; in your transcript output, so the same transcript file produces different output for different devices in an inventory.&lt;/p&gt;
&lt;p&gt;&lt;img src="../../img/2026/cisshgo-terminal-session.svg" alt="Interactive session with abbreviated command matching and Go template variables"&gt;&lt;/p&gt;
&lt;h2 id="running-it"&gt;Running It&lt;/h2&gt;
&lt;p&gt;Binary:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Default: 50 listeners starting at port 10000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./cisshgo
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Single listener on port 10022&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./cisshgo --listeners &lt;span class="m"&gt;1&lt;/span&gt; --starting-port &lt;span class="m"&gt;10022&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Multi-device inventory&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./cisshgo --inventory inventory.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Docker:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker run -d -p 10000-10049:10000-10049 ghcr.io/tbotnz/cisshgo:latest
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Every CLI flag has a corresponding environment variable (&lt;code&gt;CISSHGO_LISTENERS&lt;/code&gt;, &lt;code&gt;CISSHGO_STARTING_PORT&lt;/code&gt;, &lt;code&gt;CISSHGO_TRANSCRIPT_MAP&lt;/code&gt;, &lt;code&gt;CISSHGO_PLATFORM&lt;/code&gt;, &lt;code&gt;CISSHGO_INVENTORY&lt;/code&gt;), so it drops into a docker-compose or Kubernetes manifest without wrapper scripts.&lt;/p&gt;
&lt;h2 id="where-it-fits"&gt;Where It Fits&lt;/h2&gt;
&lt;h3 id="scale-testing"&gt;Scale Testing&lt;/h3&gt;
&lt;p&gt;This is where CiSSHGo&amp;rsquo;s architecture pays off. If you&amp;rsquo;re building or evaluating a network automation framework that needs to handle thousands of devices, you need thousands of SSH endpoints to test against. You don&amp;rsquo;t need those endpoints to actually route packets — you need them to accept connections, authenticate, and return plausible output.&lt;/p&gt;
&lt;p&gt;A single CiSSHGo process with &lt;code&gt;--listeners 10000&lt;/code&gt; spawns ten thousand SSH listeners, each running as a goroutine. Memory overhead is measured in megabytes, not gigabytes. Compare that to spinning up ten thousand virtual routers (not happening), ten thousand containers running a Python SSH server (possible but expensive), or trying to test at scale against a lab of 20 real devices and hoping the math extrapolates (it won&amp;rsquo;t — connection pooling, queue depth, and timeout behavior all change at scale).&lt;/p&gt;
&lt;p&gt;Pair CiSSHGo with an inventory file and you get a mixed-vendor topology: a few thousand IOS devices, a few thousand NX-OS, some EOS, some Junos. Your automation framework sees what looks like a heterogeneous production network. The responses are canned, but the SSH handshakes, authentication, and session management are real.&lt;/p&gt;
&lt;h3 id="cicd-pipeline-testing"&gt;CI/CD Pipeline Testing&lt;/h3&gt;
&lt;p&gt;The most straightforward use case. Drop CiSSHGo into your CI pipeline as a service container, point your automation tests at it, and validate that your code sends the right commands and parses the output correctly. No lab infrastructure to maintain, no NOS licenses to manage, no flaky device VMs timing out mid-test.&lt;/p&gt;
&lt;p&gt;A docker-compose snippet for a GitHub Actions workflow:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;cisshgo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ghcr.io/tbotnz/cisshgo:v1.0.0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;command&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;&amp;#34;--listeners&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;2&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;--starting-port&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;10022&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;10022:10022&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;10023:10023&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Your test suite connects to &lt;code&gt;localhost:10022&lt;/code&gt;, runs commands, and asserts on the output. The CiSSHGo container starts in under a second.&lt;/p&gt;
&lt;h3 id="integration-test-stacks"&gt;Integration Test Stacks&lt;/h3&gt;
&lt;p&gt;I use CiSSHGo as the mock SSH backend in the integration test suite for &lt;a href="https://github.com/lykinsbd/naas"&gt;NAAS&lt;/a&gt; (Netmiko As A Service). The docker-compose stack spins up CiSSHGo alongside the API server, workers, and Redis, and the tests exercise the full request path: HTTP request → job queue → Netmiko SSH connection to CiSSHGo → response parsing → result delivery. The CiSSHGo container handles &lt;code&gt;send_command&lt;/code&gt;, &lt;code&gt;send_config&lt;/code&gt;, structured output parsing, platform autodetect, and authentication failure scenarios — all without a real network device in the loop. I&amp;rsquo;ll cover that setup in detail in a future post.&lt;/p&gt;
&lt;h3 id="parser-development"&gt;Parser Development&lt;/h3&gt;
&lt;p&gt;If you&amp;rsquo;re writing or testing &lt;a href="https://github.com/networktocode/ntc-templates"&gt;TextFSM&lt;/a&gt; or &lt;a href="https://github.com/dmulyalin/ttp"&gt;TTP&lt;/a&gt; templates — something I&amp;rsquo;ve &lt;a href="../../posts/2020/parsing-netdevice-output-1/"&gt;written about before&lt;/a&gt; — you need consistent, known-good command output to parse against. CiSSHGo&amp;rsquo;s transcripts are sourced from NTC Templates test fixtures, so you get realistic output that matches what the parsing community already validates against. Point your parser at CiSSHGo, iterate on your template, and know that the input is stable between runs.&lt;/p&gt;
&lt;h3 id="demo-and-training-environments"&gt;Demo and Training Environments&lt;/h3&gt;
&lt;p&gt;Need to demo an automation tool without access to a lab? Need a training environment where students can SSH into &amp;ldquo;devices&amp;rdquo; without risk? CiSSHGo gives you a multi-vendor topology from a single container. It won&amp;rsquo;t teach anyone OSPF, but it will let them practice writing Ansible playbooks against something that responds like a real switch.&lt;/p&gt;
&lt;h2 id="when-not-to-use-it"&gt;When Not to Use It&lt;/h2&gt;
&lt;p&gt;Be clear-eyed about the limitations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No protocol simulation.&lt;/strong&gt; If your tests depend on BGP adjacencies forming, OSPF routes being installed, or ARP tables populating, CiSSHGo can&amp;rsquo;t help. You need CML, ContainerLab, or real hardware.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No NETCONF, gNMI, or SNMP.&lt;/strong&gt; CiSSHGo is SSH-only. If your automation uses model-driven interfaces, look elsewhere.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No dynamic state beyond scenarios.&lt;/strong&gt; Outside of scenario mode, every session gets the same output for the same command. There&amp;rsquo;s no simulated interface going up or down, no counter incrementing. Scenario mode adds ordered state changes, but it&amp;rsquo;s scripted, not reactive.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transcript maintenance.&lt;/strong&gt; Your transcripts need to match what your automation expects. If a real device&amp;rsquo;s &lt;code&gt;show version&lt;/code&gt; output changes between NOS versions and your parser depends on the new format, you need to update the transcript. This is manageable but not zero-effort.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The honest framing: CiSSHGo tests your automation&amp;rsquo;s SSH interaction layer — connection handling, command dispatch, output parsing, error handling. It doesn&amp;rsquo;t test whether your automation produces correct network state. Those are different problems, and you probably need both kinds of testing. Use CiSSHGo for the first, a real lab for the second.&lt;/p&gt;
&lt;h2 id="getting-started"&gt;Getting Started&lt;/h2&gt;
&lt;p&gt;The &lt;a href="https://github.com/tbotnz/cisshgo"&gt;GitHub repo&lt;/a&gt; has pre-built binaries on the &lt;a href="https://github.com/tbotnz/cisshgo/releases"&gt;releases page&lt;/a&gt;, Docker images on GHCR, and &lt;a href="https://tbotnz.github.io/cisshgo/"&gt;documentation&lt;/a&gt; covering configuration, transcript authoring, and the inventory system. The project is MIT licensed and accepts contributions — the &lt;a href="https://github.com/tbotnz/cisshgo/blob/master/CONTRIBUTING.md"&gt;CONTRIBUTING.md&lt;/a&gt; covers the workflow.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re already testing network automation against real devices or heavyweight simulators and finding it slow, flaky, or expensive to maintain, CiSSHGo is worth a look. It won&amp;rsquo;t replace your lab, but it might mean you only need the lab for the tests that actually require one.&lt;/p&gt;</description></item></channel></rss>