{"article":{"slug":"self-hosted-http-tunnels-with-ssh-and-nginx","title":"Self-hosted HTTP tunnels with SSH and nginx","subtitle":null,"summary":"Vincent Bernat shows how to expose a localhost preview to a friend with only a plain SSH client and an nginx server you control: ssh -R with a dynamic port, an nginx server_name regex mapping per-port subdomains, wildcard TLS via ACME DNS-01, and a helper script plus NixOS module.","content_type":"tutorial","language":"en","canonical_url":"https://vincent.bernat.ch/en/blog/2026-http-over-ssh","author":{"name":"Vincent Bernat","url":"https://vincent.bernat.ch/","person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"vincent.bernat.ch","url":"https://vincent.bernat.ch/","listing_slug":null,"listing":null},"topics":[{"name":"Networking","slug":"networking","url":"https://listedarticles.com/topics/networking"},{"name":"Linux","slug":"linux","url":"https://listedarticles.com/topics/linux"},{"name":"DevOps","slug":"devops","url":"https://listedarticles.com/topics/devops"},{"name":"Tutorials","slug":"tutorials","url":"https://listedarticles.com/topics/tutorials"},{"name":"Infrastructure","slug":"infrastructure","url":"https://listedarticles.com/topics/infrastructure"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":998,"reading_minutes":4,"published_at":"2026-10-03T00:00:00.000Z","added_at":"2026-10-04T23:17:18.105Z","updated_at":"2026-10-04T23:17:18.105Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/self-hosted-http-tunnels-with-ssh-and-nginx","markdown_url":"https://listedarticles.com/articles/self-hosted-http-tunnels-with-ssh-and-nginx.md","example":false,"citation":"Vincent Bernat, vincent.bernat.ch. \"Self-hosted HTTP tunnels with SSH and nginx.\" 3 Oct 2026. https://vincent.bernat.ch/en/blog/2026-http-over-ssh (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://vincent.bernat.ch/en/blog/2026-http-over-ssh"},"body_markdown":"A friend wants to proofread your work-in-progress blog post, but its preview\nonly runs on `localhost:8080`. [Several tools](https://github.com/anderspitman/awesome-tunneling) can help. Some run as a\ncommercial service, like [ngrok](https://ngrok.com/) or [Cloudflare Quick Tunnels](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/trycloudflare/). Some are\nself-hostable but require a specific client, like [frp](https://github.com/fatedier/frp) or [localtunnel](https://github.com/localtunnel/localtunnel).\nSome only require a plain SSH client but rely on a specific SSH server, like\n[sish](https://docs.ssi.sh/). Let’s implement a self-hosted solution with only *OpenSSH* and\n*nginx*!\n\n```\n$ ssh -R 0:localhost:8080 http-over-ssh\nAllocated port 41535 for remote forward to localhost:8080\nhttps://6J3jK1WmB15c6WmjW_X-Wg--1789928654@p41535.ssh.luffy.cx/\n```\n# Basic setup[#](https://vincent.bernat.ch#basic-setup)\n\nFirst, we forward connections from a port on a remote server to your local service:\n\n```\n$ ssh -N -R 0:localhost:8080 web02.luffy.cx\nAllocated port 41535 for remote forward to localhost:8080\n```\nWhen you specify `0` as the remote port, the server allocates a free port.\nThen, we configure nginx to proxy requests from `https://p41535.ssh.luffy.cx` to\n`http://127.0.0.1:41535`:\n\n```\nserver {\n  listen 0.0.0.0:443 ssl ;\n  listen [::0]:443 ssl ;\n  server_name ~^p(?<port>\\d\\d\\d\\d\\d)\\.ssh\\.luffy\\.cx$;\n  location / {\n    proxy_pass http://127.0.0.1:$port;\n  }\n}\n```\nWe also need to add DNS records for `*.ssh.luffy.cx` and get a wildcard\ncertificate through [Let’s Encrypt](https://letsencrypt.org/):\n\n```\n*.ssh.luffy.cx.               CNAME web02.luffy.cx.\nssh.luffy.cx.                 CAA   0 issuewild \"letsencrypt.org\"\n_acme-challenge.ssh.luffy.cx  CNAME ssh.luffy.cx.acme.luffy.cx.\n```\n`acme.luffy.cx` is a zone hosted on Route 53. I use it for [ACME DNS-01\nchallenges](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge), both for wildcard certificates and for domains served by\nseveral web servers. In my case, NixOS [gets the certificates\nautomatically](https://wiki.nixos.org/wiki/ACME).\n\n# Access control[#](https://vincent.bernat.ch#access-control)\n\nThe port is the only “secret”<sup>[1](https://vincent.bernat.ch#sidenote-port)</sup> keeping the content confidential. Other\nforwarding solutions add a random string to the domain name to prevent an\nintruder from enumerating the possible values.\n\nThanks to [`ngx_http_secure_link_module`](https://nginx.org/en/docs/http/ngx_http_secure_link_module.html), we can secure\nthis setup a bit. This module computes a hash<sup>[2](https://vincent.bernat.ch#sidenote-md5)</sup> over a set of values,\nincluding a secret, and compares it with the hash from the request. The hash is\nbase64-encoded, so we cannot put it in the domain name, which is\ncase-insensitive. Instead, we put it in the URL as a username, along with its\nexpiration timestamp:[3](https://vincent.bernat.ch#sidenote-expiration)\n\n```\nhttps://6J3jK1WmB15c6WmjW_X-Wg--1789928654@p41535.ssh.luffy.cx/en/blog\n        ╰─────────┬──────────╯  ╰───┬────╯  ╰─┬─╯             ╰──┬───╯\n                hash             expires    port               path\n```\nThe client sends the username to the server with [HTTP basic\nauthentication](https://www.rfc-editor.org/rfc/rfc7617). This works with most HTTP clients, including `curl`.\nNginx exposes the username in the `$remote_user` variable. The module expects\nthe hash and the expiration timestamp separated by a comma. We use a `map`\ndirective to extract the two parts from `$remote_user` and join them with a\ncomma.<sup>[4](https://vincent.bernat.ch#sidenote-comma)</sup> We also give the module the string to hash. It contains the\nexpiration timestamp, the port, and a secret:\n\n```\nmap $remote_user $httpssh_link {\n  \"~^([-_A-Za-z0-9]{22})--([0-9]+)$\" \"$1,$2\";\n}\nserver {\n  # […]\n  location / {\n    secure_link $httpssh_link;\n    secure_link_md5 \"$secure_link_expires $port ZuPerS3cr3!\";\n  }\n}\n```\nThe module returns the status of the check in the `$secure_link` variable:\n\n- empty if the hashes do not match,\n- `\"0\"` if they match but the link has expired, or\n- `\"1\"` otherwise.\n\nIf the hash is incorrect or missing, we return a 401 error with a\n`WWW-Authenticate` header to ask for credentials. If the link has expired, we\nreturn a 410 error. We remove the `Authorization` header before forwarding the\nrequest and add a few directives to [proxy WebSocket connections](https://nginx.org/en/docs/http/websocket.html).\nHere is the complete configuration:[5](https://vincent.bernat.ch#sidenote-security)\n\n```\nmap $remote_user $httpssh_link {\n  \"~^([-_A-Za-z0-9]{22})--([0-9]+)$\" \"$1,$2\";\n}\nserver {\n  listen 0.0.0.0:443 ssl ;\n  listen [::0]:443 ssl ;\n  server_name ~^p(?<port>\\d\\d\\d\\d\\d)\\.ssh\\.luffy\\.cx$;\n  location / {\n    secure_link $httpssh_link;\n    secure_link_md5 \"$secure_link_expires $port ZuPerS3cr3!\";\n    if ($secure_link = \"\") {\n      add_header WWW-Authenticate 'Basic realm=\"tunnel\"' always;\n      return 401;\n    }\n    if ($secure_link = \"0\") {\n      return 410;\n    }\n    proxy_pass http://127.0.0.1:$port;\n    proxy_set_header Host $host;\n    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    proxy_set_header Authorization \"\";\n    proxy_http_version 1.1;\n    proxy_set_header Upgrade $http_upgrade;\n    proxy_set_header Connection \"upgrade\";\n    proxy_buffering off;\n    proxy_read_timeout 30m;\n  }\n}\n```\nI think you are now asking yourself the obvious question: “How should I generate the hash?” Easy peasy!\n\n```\n$ expires=$(( $(date +%s) + 86400 ))\n$ port=41535\n$ secret='ZuPerS3cr3!'\n$ printf '%s %s %s' \"$expires\" \"$port\" \"$secret\" \\\n>   | openssl md5 -binary \\\n>   | openssl base64 \\\n>   | tr +/ -_ | tr -d =\n6J3jK1WmB15c6WmjW_X-Wg\n```\nWell, I suppose you are now saying: “Vincent, this is not very convenient! I’ll stick with ngrok if you don’t mind.” Okay, I hear you. Let’s write a helper script.\n\n# Helper script[#](https://vincent.bernat.ch#helper-script)\n\nThe main difficulty is finding the ephemeral port that OpenSSH allocates, as it\ndoes not appear in any environment variable.<sup>[6](https://vincent.bernat.ch#sidenote-env)</sup> To work around this obstacle,\nwe look for the ancestor `sshd-session` processes:[7](https://vincent.bernat.ch#sidenote-sshd-session)\n\n```\npids=$(\n  pid=$$\n  while [ \"$pid\" -gt 1 ]; do\n    line=$(ps -o comm=,pid=,ppid= -p \"$pid\")\n    echo \"$line\"\n    pid=${line##* }\n  done | awk '$1 == \"sshd-session\" { printf \"pid=%s,\\n\", $2 }'\n)\nif [ -z \"$pids\" ]; then\n  echo \"not an ssh session\" >&2\n  exit 1\nfi\n```\nThen, we get the listening ports associated with these `sshd-session`\nprocesses:[8](https://vincent.bernat.ch#sidenote-sudo)\n\n```\nports=$(sudo -n ss --listening --numeric --tcp --processes --no-header \\\n  | grep -F \"$pids\" \\\n  | awk '{ print $4 }' | awk -F: '{ print $NF }' \\\n  | sort -un)\nif [ -z \"$ports\" ]; then\n  echo \"no forwarded port, use ssh -R 0:localhost:PORT\" >&2\n  exit 1\nfi\n```\nFinally, we display the URLs and keep the session open:\n\n```\nlifetime=86400\nsecret='ZuPerS3cr3!'\nexpires=$(( $(date +%s) + lifetime ))\nfor port in $ports; do\n  token=$(printf '%s %s %s' \"$expires\" \"$port\" \"$secret\" \\\n            | openssl md5 -binary \\\n            | openssl base64 \\\n            | tr +/ -_ | tr -d =)\n  echo \"https://$token--$expires@p$port.ssh.luffy.cx/\"\ndone\nsleep infinity\n```\nI install this script as `http-over-ssh` on the server and add this entry to my\n`~/.ssh/config`:\n\n```\nHost http-over-ssh\n  Hostname web02.luffy.cx\n  RemoteCommand http-over-ssh\n  ControlPath none\n```\nWith this solution, I only rely on OpenSSH and nginx, two pieces of software\nalready running on this server. One short command gives me a self-hosted tunnel\nand a URL to share. To try it, grab the [complete helper script](https://github.com/vincentbernat/nixops-take1/blob/master/tags/http-over-ssh.sh), which\nincludes a few minor improvements. If you run NixOS, as any person of taste\nwould, have a look at my [`http-over-ssh.nix`](https://github.com/vincentbernat/nixops-take1/blob/master/tags/http-over-ssh.nix) instead. ❄️\n","body_html":"<p>A friend wants to proofread your work-in-progress blog post, but its preview\nonly runs on <code>localhost:8080</code>. <a href=\"https://github.com/anderspitman/awesome-tunneling\" rel=\"nofollow ugc noopener\">Several tools</a> can help. Some run as a\ncommercial service, like <a href=\"https://ngrok.com/\" rel=\"nofollow ugc noopener\">ngrok</a> or <a href=\"https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/trycloudflare/\" rel=\"nofollow ugc noopener\">Cloudflare Quick Tunnels</a>. Some are\nself-hostable but require a specific client, like <a href=\"https://github.com/fatedier/frp\" rel=\"nofollow ugc noopener\">frp</a> or <a href=\"https://github.com/localtunnel/localtunnel\" rel=\"nofollow ugc noopener\">localtunnel</a>.\nSome only require a plain SSH client but rely on a specific SSH server, like\n<a href=\"https://docs.ssi.sh/\" rel=\"nofollow ugc noopener\">sish</a>. Let’s implement a self-hosted solution with only <em>OpenSSH</em> and\n<em>nginx</em>!</p>\n<pre><code>$ ssh -R 0:localhost:8080 http-over-ssh\nAllocated port 41535 for remote forward to localhost:8080\nhttps://6J3jK1WmB15c6WmjW_X-Wg--1789928654@p41535.ssh.luffy.cx/</code></pre>\n<h1 id=\"basic-setup\">Basic setup<a href=\"https://vincent.bernat.ch#basic-setup\" rel=\"nofollow ugc noopener\">#</a></h1>\n<p>First, we forward connections from a port on a remote server to your local service:</p>\n<pre><code>$ ssh -N -R 0:localhost:8080 web02.luffy.cx\nAllocated port 41535 for remote forward to localhost:8080</code></pre>\n<p>When you specify <code>0</code> as the remote port, the server allocates a free port.\nThen, we configure nginx to proxy requests from <code>https://p41535.ssh.luffy.cx</code> to\n<code>http://127.0.0.1:41535</code>:</p>\n<pre><code>server {\n  listen 0.0.0.0:443 ssl ;\n  listen [::0]:443 ssl ;\n  server_name ~^p(?&lt;port&gt;\\d\\d\\d\\d\\d)\\.ssh\\.luffy\\.cx$;\n  location / {\n    proxy_pass http://127.0.0.1:$port;\n  }\n}</code></pre>\n<p>We also need to add DNS records for <code>*.ssh.luffy.cx</code> and get a wildcard\ncertificate through <a href=\"https://letsencrypt.org/\" rel=\"nofollow ugc noopener\">Let’s Encrypt</a>:</p>\n<pre><code>*.ssh.luffy.cx.               CNAME web02.luffy.cx.\nssh.luffy.cx.                 CAA   0 issuewild &quot;letsencrypt.org&quot;\n_acme-challenge.ssh.luffy.cx  CNAME ssh.luffy.cx.acme.luffy.cx.</code></pre>\n<p><code>acme.luffy.cx</code> is a zone hosted on Route 53. I use it for <a href=\"https://letsencrypt.org/docs/challenge-types/#dns-01-challenge\" rel=\"nofollow ugc noopener\">ACME DNS-01\nchallenges</a>, both for wildcard certificates and for domains served by\nseveral web servers. In my case, NixOS <a href=\"https://wiki.nixos.org/wiki/ACME\" rel=\"nofollow ugc noopener\">gets the certificates\nautomatically</a>.</p>\n<h1 id=\"access-control\">Access control<a href=\"https://vincent.bernat.ch#access-control\" rel=\"nofollow ugc noopener\">#</a></h1>\n<p>The port is the only “secret”&lt;sup&gt;<a href=\"https://vincent.bernat.ch#sidenote-port\" rel=\"nofollow ugc noopener\">1</a>&lt;/sup&gt; keeping the content confidential. Other\nforwarding solutions add a random string to the domain name to prevent an\nintruder from enumerating the possible values.</p>\n<p>Thanks to <a href=\"https://nginx.org/en/docs/http/ngx_http_secure_link_module.html\" rel=\"nofollow ugc noopener\"><code>ngx_http_secure_link_module</code></a>, we can secure\nthis setup a bit. This module computes a hash&lt;sup&gt;<a href=\"https://vincent.bernat.ch#sidenote-md5\" rel=\"nofollow ugc noopener\">2</a>&lt;/sup&gt; over a set of values,\nincluding a secret, and compares it with the hash from the request. The hash is\nbase64-encoded, so we cannot put it in the domain name, which is\ncase-insensitive. Instead, we put it in the URL as a username, along with its\nexpiration timestamp:<a href=\"https://vincent.bernat.ch#sidenote-expiration\" rel=\"nofollow ugc noopener\">3</a></p>\n<pre><code>https://6J3jK1WmB15c6WmjW_X-Wg--1789928654@p41535.ssh.luffy.cx/en/blog\n        ╰─────────┬──────────╯  ╰───┬────╯  ╰─┬─╯             ╰──┬───╯\n                hash             expires    port               path</code></pre>\n<p>The client sends the username to the server with <a href=\"https://www.rfc-editor.org/rfc/rfc7617\" rel=\"nofollow ugc noopener\">HTTP basic\nauthentication</a>. This works with most HTTP clients, including <code>curl</code>.\nNginx exposes the username in the <code>$remote_user</code> variable. The module expects\nthe hash and the expiration timestamp separated by a comma. We use a <code>map</code>\ndirective to extract the two parts from <code>$remote_user</code> and join them with a\ncomma.&lt;sup&gt;<a href=\"https://vincent.bernat.ch#sidenote-comma\" rel=\"nofollow ugc noopener\">4</a>&lt;/sup&gt; We also give the module the string to hash. It contains the\nexpiration timestamp, the port, and a secret:</p>\n<pre><code>map $remote_user $httpssh_link {\n  &quot;~^([-_A-Za-z0-9]{22})--([0-9]+)$&quot; &quot;$1,$2&quot;;\n}\nserver {\n  # […]\n  location / {\n    secure_link $httpssh_link;\n    secure_link_md5 &quot;$secure_link_expires $port ZuPerS3cr3!&quot;;\n  }\n}</code></pre>\n<p>The module returns the status of the check in the <code>$secure_link</code> variable:</p>\n<ul><li>empty if the hashes do not match,</li><li><code>&quot;0&quot;</code> if they match but the link has expired, or</li><li><code>&quot;1&quot;</code> otherwise.</li></ul>\n<p>If the hash is incorrect or missing, we return a 401 error with a\n<code>WWW-Authenticate</code> header to ask for credentials. If the link has expired, we\nreturn a 410 error. We remove the <code>Authorization</code> header before forwarding the\nrequest and add a few directives to <a href=\"https://nginx.org/en/docs/http/websocket.html\" rel=\"nofollow ugc noopener\">proxy WebSocket connections</a>.\nHere is the complete configuration:<a href=\"https://vincent.bernat.ch#sidenote-security\" rel=\"nofollow ugc noopener\">5</a></p>\n<pre><code>map $remote_user $httpssh_link {\n  &quot;~^([-_A-Za-z0-9]{22})--([0-9]+)$&quot; &quot;$1,$2&quot;;\n}\nserver {\n  listen 0.0.0.0:443 ssl ;\n  listen [::0]:443 ssl ;\n  server_name ~^p(?&lt;port&gt;\\d\\d\\d\\d\\d)\\.ssh\\.luffy\\.cx$;\n  location / {\n    secure_link $httpssh_link;\n    secure_link_md5 &quot;$secure_link_expires $port ZuPerS3cr3!&quot;;\n    if ($secure_link = &quot;&quot;) {\n      add_header WWW-Authenticate &#39;Basic realm=&quot;tunnel&quot;&#39; always;\n      return 401;\n    }\n    if ($secure_link = &quot;0&quot;) {\n      return 410;\n    }\n    proxy_pass http://127.0.0.1:$port;\n    proxy_set_header Host $host;\n    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    proxy_set_header Authorization &quot;&quot;;\n    proxy_http_version 1.1;\n    proxy_set_header Upgrade $http_upgrade;\n    proxy_set_header Connection &quot;upgrade&quot;;\n    proxy_buffering off;\n    proxy_read_timeout 30m;\n  }\n}</code></pre>\n<p>I think you are now asking yourself the obvious question: “How should I generate the hash?” Easy peasy!</p>\n<pre><code>$ expires=$(( $(date +%s) + 86400 ))\n$ port=41535\n$ secret=&#39;ZuPerS3cr3!&#39;\n$ printf &#39;%s %s %s&#39; &quot;$expires&quot; &quot;$port&quot; &quot;$secret&quot; \\\n&gt;   | openssl md5 -binary \\\n&gt;   | openssl base64 \\\n&gt;   | tr +/ -_ | tr -d =\n6J3jK1WmB15c6WmjW_X-Wg</code></pre>\n<p>Well, I suppose you are now saying: “Vincent, this is not very convenient! I’ll stick with ngrok if you don’t mind.” Okay, I hear you. Let’s write a helper script.</p>\n<h1 id=\"helper-script\">Helper script<a href=\"https://vincent.bernat.ch#helper-script\" rel=\"nofollow ugc noopener\">#</a></h1>\n<p>The main difficulty is finding the ephemeral port that OpenSSH allocates, as it\ndoes not appear in any environment variable.&lt;sup&gt;<a href=\"https://vincent.bernat.ch#sidenote-env\" rel=\"nofollow ugc noopener\">6</a>&lt;/sup&gt; To work around this obstacle,\nwe look for the ancestor <code>sshd-session</code> processes:<a href=\"https://vincent.bernat.ch#sidenote-sshd-session\" rel=\"nofollow ugc noopener\">7</a></p>\n<pre><code>pids=$(\n  pid=$$\n  while [ &quot;$pid&quot; -gt 1 ]; do\n    line=$(ps -o comm=,pid=,ppid= -p &quot;$pid&quot;)\n    echo &quot;$line&quot;\n    pid=${line##* }\n  done | awk &#39;$1 == &quot;sshd-session&quot; { printf &quot;pid=%s,\\n&quot;, $2 }&#39;\n)\nif [ -z &quot;$pids&quot; ]; then\n  echo &quot;not an ssh session&quot; &gt;&amp;2\n  exit 1\nfi</code></pre>\n<p>Then, we get the listening ports associated with these <code>sshd-session</code>\nprocesses:<a href=\"https://vincent.bernat.ch#sidenote-sudo\" rel=\"nofollow ugc noopener\">8</a></p>\n<pre><code>ports=$(sudo -n ss --listening --numeric --tcp --processes --no-header \\\n  | grep -F &quot;$pids&quot; \\\n  | awk &#39;{ print $4 }&#39; | awk -F: &#39;{ print $NF }&#39; \\\n  | sort -un)\nif [ -z &quot;$ports&quot; ]; then\n  echo &quot;no forwarded port, use ssh -R 0:localhost:PORT&quot; &gt;&amp;2\n  exit 1\nfi</code></pre>\n<p>Finally, we display the URLs and keep the session open:</p>\n<pre><code>lifetime=86400\nsecret=&#39;ZuPerS3cr3!&#39;\nexpires=$(( $(date +%s) + lifetime ))\nfor port in $ports; do\n  token=$(printf &#39;%s %s %s&#39; &quot;$expires&quot; &quot;$port&quot; &quot;$secret&quot; \\\n            | openssl md5 -binary \\\n            | openssl base64 \\\n            | tr +/ -_ | tr -d =)\n  echo &quot;https://$token--$expires@p$port.ssh.luffy.cx/&quot;\ndone\nsleep infinity</code></pre>\n<p>I install this script as <code>http-over-ssh</code> on the server and add this entry to my\n<code>~/.ssh/config</code>:</p>\n<pre><code>Host http-over-ssh\n  Hostname web02.luffy.cx\n  RemoteCommand http-over-ssh\n  ControlPath none</code></pre>\n<p>With this solution, I only rely on OpenSSH and nginx, two pieces of software\nalready running on this server. One short command gives me a self-hosted tunnel\nand a URL to share. To try it, grab the <a href=\"https://github.com/vincentbernat/nixops-take1/blob/master/tags/http-over-ssh.sh\" rel=\"nofollow ugc noopener\">complete helper script</a>, which\nincludes a few minor improvements. If you run NixOS, as any person of taste\nwould, have a look at my <a href=\"https://github.com/vincentbernat/nixops-take1/blob/master/tags/http-over-ssh.nix\" rel=\"nofollow ugc noopener\"><code>http-over-ssh.nix</code></a> instead. ❄️</p>","headings":[{"level":1,"text":"Basic setup#","id":"basic-setup"},{"level":1,"text":"Access control#","id":"access-control"},{"level":1,"text":"Helper script#","id":"helper-script"}]}}