I’ve been working on a lot of documentation lately, and I’ve been trying to adhere to standards by linting my Markdown in VS Code the same way I lint my code. That being said, I’ve grown weary of fighting the linter over a few rules.
Note: These instructions assume you are using macOS.
Configure VS Code markdownlint with rules intended for GitHub Flavored Markdown (GFM). These settings provide sensible defaults for writing README files and other Markdown documentation hosted on GitHub.
Create the Configuration File
Create a global .markdownlint.json file in your home directory:
A project-level .markdownlint.json can be committed to Git with the rest of the project. This is especially useful for team projects because everyone who clones the repository can use the same Markdownlint rules.
It also allows individual projects to define their own Markdown standards instead of relying on each developer’s global VS Code configuration.
I use Mailpit to capture email while developing CFML applications locally. It works great, but I don’t necessarily need it running all the time. Since I already use CommandBox to start and stop my local ColdFusion server, I wanted Mailpit to follow the same lifecycle.
Fortunately, CommandBox server scripts make this straightforward. When the CommandBox server starts, it can start Mailpit. When the server stops, it can stop Mailpit as well.
Prerequisites
This example is for macOS and assumes Mailpit was installed with Homebrew:
The ! prefix tells CommandBox to execute a native operating-system command.
If CommandBox cannot find Homebrew in its PATH, use the full path to the executable. On an Apple Silicon Mac, that will commonly be /opt/homebrew/bin/brew:
On an Intel Mac, Homebrew is commonly located at /usr/local/bin/brew. Run which brew in Terminal to confirm the correct path.
Wait until Mailpit is ready
Starting the Homebrew service does not necessarily mean Mailpit is ready to accept requests at that exact instant. Mailpit provides /readyz for checking its readiness. A successful request returns an HTTP 200 response.
We can make the startup script check that endpoint once per second for up to 20 seconds:
{
"scripts": {
"onServerStart": [
"!brew services start mailpit",
"!for i in {1..20}; do curl -fsS http://127.0.0.1:8025/readyz && exit 0; sleep 1; done; echo 'Mailpit failed to become ready' >&2; exit 1"
],
"onServerStop": "!brew services stop mailpit"
}
}
This script:
Starts Mailpit through Homebrew.
Checks the Mailpit readiness endpoint.
Continues as soon as Mailpit responds successfully.
Returns an error if Mailpit does not become ready within 20 seconds.
You can also check Mailpit manually at any time:
curl -fsS http://127.0.0.1:8025/readyz
To inspect the Homebrew service state instead, run:
brew services info mailpit
Show Mailpit’s status on a local home page
My CommandBox server hosts several local applications, so I have a simple home page that links to each application and development resource. I wanted its Mailpit card to show whether Mailpit was online.
When Mailpit is stopped, the card displays a red Offline badge:
Mailpit Offline
When Mailpit is available, it displays a green Online badge:
Mailpit Online
First, check the readiness endpoint near the beginning of the CFML page, before the HTML output:
The default value is false. If Mailpit responds with HTTP status 200, it changes to true. A timeout or connection error is caught so an unavailable Mailpit service does not cause the home page itself to fail.
The two-second timeout also prevents the check from delaying the page for too long.
Next, use that value to set the card’s border and status badge:
This example uses Bootstrap 5 classes for the card, border, and badge styling.
Putting it all together
The final CommandBox configuration is:
"scripts": {
"onServerStart": [
"!brew services start mailpit",
"!for i in {1..20}; do curl -fsS http://127.0.0.1:8025/readyz && exit 0; sleep 1; done; echo 'Mailpit failed to become ready' >&2; exit 1"
],
"onServerStop": "!brew services stop mailpit"
}
Now Mailpit starts along with the CommandBox server, its readiness is verified, and the local home page indicates whether it is online. When the CommandBox server is stopped normally, Mailpit is stopped as well.
A few things to keep in mind
The stop hook runs when the server is stopped through CommandBox. It cannot run if CommandBox is force-terminated or the computer shuts down unexpectedly.
Homebrew services are user-wide. If several CommandBox servers or other applications share the same Mailpit instance, stopping one server could stop Mailpit while something else is using it.
If one CommandBox server hosts several applications, put the scripts in that server’s shared configuration. Mailpit will then follow the lifecycle of the entire server rather than any single application.
The status badge reflects Mailpit’s state when the CFML page is rendered. Refresh the page to update it.
That’s it. Mailpit is available when the local development server is running and gets out of the way when development is finished.
I’ve always been a fan of the tray that’s available when CommandBox launches. On the Mac, it appears as a menu bar icon in the top menu (first icon below).
CommandBox menu bar icon
It has some very convenient built-in options:
Servername
├── Stop Server
├── Restart Server
├── Open...
│ ├── Webroot
│ ├── Server Home
│ ├── Site Home
│ └── Server Admin
├── Info
│ ├── Engine: adobe 2023.0.17+330864
│ ├── Webroot: /Path/to/website/
│ ├── URL: https://127.0.0.1:8443
│ ├── PID: 99999
│ └── Heap: Not set
I frequently need to access Mailpit and various log files as I work through the development process on a current project. This got me wondering if I could add some of my own frequently accessed items to the tray. It turns out you can add quite a few different things.
Enable the tray
First, enable the tray in the server.json file for the site.
"trayEnable": true,
Next, let’s start simple and add a single item to open Mailpit in a browser. The trayOptions setting accepts an array of objects. Each object should contain a label, an action, and one of [url, path, command]. The label is the text that appears in the tray. The action tells CommandBox what to perform (openbrowser, openfilesystem, runAsync, etc.). The [url, path, command] values provide the URL of the site, the filesystem path, or the command to execute, respectively.
This command creates a temporary shell script, makes it executable, and opens it in Terminal. Reminder: this version is macOS only as written because it uses zsh, mktemp, and open -a Terminal.
Mail Server: 127.0.0.1
Server Port: 1025
Username: blank
Password: blank
Use TLS: unchecked
Use SSL: unchecked
Test Email Delivery
Create a .cfm test page.
<cfscript>
recipientCount = 5;
for (i = 1; i <= recipientCount; i++) {
recipient = "foo#i#@bar.com";
cfmail(
to = recipient,
from = "[email protected]",
subject = "Mailpit Test",
type = "html"
) {
writeOutput("
<h1>Hello from ColdFusion</h1>
<p>
It is #dateFormat(now(), 'mm/dd/yyyy')# at
#dateTimeFormat(now(), 'hh:mm:ss tt')#.
This should show up in Mailpit.
</p>
");
}
sleep(3000);
writeOutput("<p>Mail sent.</p>");
}
writeOutput('<p><a href="#cgi.script_name#">Run again</a></p>');
</cfscript>
Verify Email Delivery
Open the Mailpit web interface:
http://localhost:8025
The test emails should appear in the Mailpit inbox.
Mailpit inbox
Restore Original Mail Settings (Optional)
Restore the original ColdFusion mail configuration.
It’s been ages since I have had the time to tinker with any of my Raspberry Pis. In fact playing old games with my son using Retro Pi is probably 90% of the interaction in the last 3-4 years. Last week I was cleaning up some old “tech stuff” around my home office and came across my Raspberry Pi 5 unplugged and shelved. Barely an hour later I was running ColdFusion on it (of course by way of CommandBox).
I’m not going to go through all the details on how to get a Raspberry Pi up and running. There are plenty of videos and tutorials for that. What I’d like to
provide is a slight update to the 4 commands necessary to install CommandBox on the current OS – Raspbian GNU/Linux 13 (trixie). I found this in the Linux apt-get → Stable section of the CommandBox documentation. I added the comments.
# 1. Downloads Ortus Solutions' repository GPG signing key,
# converts it to a trusted keyring format, and installs it
# so your system can verify packages from that repo are authentic.
curl -fsSl https://downloads.ortussolutions.com/debs/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/ortussolutions.gpg > /dev/null
# 2. Adds the Ortus Solutions package repository to your system's
# APT software sources list.
echo "deb [signed-by=/usr/share/keyrings/ortussolutions.gpg] https://downloads.ortussolutions.com/debs/noarch /" \
| sudo tee /etc/apt/sources.list.d/commandbox.list
# 3. Refreshes the package index and installs CommandBox
# (plus HTTPS support for APT if needed).
sudo apt-get update && sudo apt-get install apt-transport-https commandbox
# 4. Install Java
sudo apt install default-jdk
Number 4 is really my only contribution here. The original command: sudo apt install openjdk-11-jdk results in an error since openjdk-11-jdk no longer exists in the main repository. The command I provided installs the default which at the time of this writing is openjdk-21. Below you can see the output of the original command and the updated one.
Install Java
Once CommandBox was installed I fired up an instance of Adobe ColdFusion 2025. Screenshot was taken using Raspberry Pi Connect screen sharing. As a side note I
showed server.os.additionalInformation for ACF since server.os.name reports as UNIX.
ACF ColdFusion on Raspberry Pi 5
Also fired up an instance of Lucee 5. Screenshot taken again using Raspberry Pi Connect screen sharing.
Lucee ColdFusion on Raspberry Pi 5
I guess I’ve been away from the Raspberry Pi game for so long that I was pretty blown away by the new Raspberry Pi Connect remote-access service from the Raspberry Pi Foundation. This service lets you open and control your Raspberry Pi desktop from anywhere through a web browser — without port-forwarding, VPNs, or knowing your home IP address.
Also of note, the Raspberry Pi Imager is amazing. You can preconfigure settings such as WiFi network access, VNC, Hostname, and other settings as you create the image rather than digging around in config files or using raspi-config after the fact.
This is an example of using the Better Comments extension for CFML development in VSCode .
The Better Comments extension visually highlights specially tagged comments (like TODO, !, or ?) using colors and styles so important notes, questions, and tasks stand out instantly in your code.
You can define custom styles in either User or Workspace settings.json. You can set the color or backgroundColor, as well as underline, strikethrough, bold, and italic.
Gotcha 1: You must define all better-comments.tags in one location. Better Comments does not merge tag arrays.
Gotcha 2: When using CFML block comments in tag-based ColdFusion files, you must use // as the first non-whitespace characters after the opening comment tag for Better Comments to recognize the comment type.
Changes to settings may require reloading the VS Code window to take effect:
CMD+SHIFT+P > Developer: Reload Window
Results below:
Better Comments extension for CFML development in VSCode
Security headers can be a frequently failed item in WordPress Site Health. The error in Site Health is:
Not all essential security headers are installed
All of these headers can be set at CloudFlare:
Upgrade Insecure Requests
X-XSS protection
X-Content Type Options
Referrer-Policy
Permissions-Policy
HTTP Strict Transport Security
Review site headers using curl
To see what headers are being returned from a website use the following command: curl -I foo.com
curl is a command-line tool for making HTTP/HTTPS requests. The request defaults to https.
The -I parameter tells curl to:
Send an HTTP HEAD request instead of GET
Return headers only
Skip downloading the response body (HTML, JSON, etc.)
curl command
Cloudflare – Add a Response Header Transform Rule
Log in to Cloudflare and select a domain.
Go to Rules → Overview.
Click Create rule and choose Response Header Transform Rules
Give the rule a clear name. Example: Security Headers (WordPress Site Health)
Set All incoming requests to Apply this rule to all requests.
Under Then, choose Set static and enter the Header name and Value you want to add or modify.
Click Set new header to add additional headers if needed
Click Deploy to activate the rule.
Response Header Transform Rule
Recommended Header Name and Value Settings
Header name
Value
Set static
Content-Security-Policy
upgrade-insecure-requests
Set static
Permissions-Policy
geolocation=(), microphone=(), camera=()
Set static
Strict-Transport-Security
max-age=31536000; includeSubDomains; preload
Set static
X-Content-Type-Options
nosniff
Set static
X-XSS-Protection
1; mode=block
Set static
strict-origin-when-cross-origin
Referrer-Policy
Settings Explanation
Content-Security-Policy: upgrade-insecure-requests — Automatically upgrades all HTTP resource requests to HTTPS to prevent mixed-content issues and improve transport security.
Permissions-Policy: geolocation=(), microphone=(), camera=() — Explicitly disables access to geolocation, microphone, and camera APIs for the site and all embedded content.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload — Forces browsers to use HTTPS only for one year for the site and all subdomains and signals eligibility for browser HSTS preload lists. (Note: the header name should beStrict-Transport-Security*.)*
X-Content-Type-Options: nosniff — Prevents browsers from MIME-sniffing responses and forces them to respect the declared Content-Type.
X-XSS-Protection: 1; mode=block — Enables the browser’s legacy XSS filter and blocks rendering of the page if an attack is detected.
Referrer-Policy: strict-origin-when-cross-origin — Sends the full referrer URL for same-origin requests but only the origin for cross-origin requests, and nothing when downgrading from HTTPS to HTTP.⠀
Confirm Response Header Transform Rule
To confirm the new headers are being returned use the curl command from earlier:
Use of a weak hashing algorithm such as MD5 (the algorithm used by CFMX_COMPAT). This can also be a compatibility issue (after CF2023 update 8 and CF2021 update 14) if the hash algorithm is not specified. The default has changed from CFMX_COMPAT to SHA-256 in those releases.
In CFML, Hash() can appear in two contexts:
Output expressions: #Hash(value)#
Script/logic: Hash(value)
Any global refactor must account for both forms. It took me a few iterations to get what I needed.
This article demonstrates how to perform a global search and replace using REGEX in VS Code.
Note(s):
In the VS Code search panel REGEX is enabled with the .* icon to the right of the search input.
The ColdFusion app I was working with used only Hash() and not hash().
You could use a case insensitive search with the REGEX from Iteration 3 with “Preserve Case” for the replace input to account for Hash() vs hash() if necessary.
Your mileage may vary on this solution.
WARNING
PLEASE PREVIEW THE RESULTS OF YOUR SEARCHES BEFORE DOING THE REPLACE.
Iteration 1
This was my first attempt.
Search:#Hash\(\s*([^)]*?)\s*\)# Replace:#Hash($1, "SHA-256", "UTF-8")# Bad Match:<a href="edit.cfm?newsID=#qData.newsID#&verifyID=#Hash(qData.newsID, ">Edit</a> Bad Result:<a href="edit.cfm?newsID=#qData.newsID#&verifyID=#Hash(qData.newsID, ">Edit</a> Why it’s bad: Caused incorrect code if the algorithm argument already existed.
Iteration 2
Based on the failure of the first attempt I made the following second attempt.
cfhash search and replace with regex vscode screenshot
Conclusion
YAY! This result yielded 206 corrections throughout the app that would have taken a long time to correct without a REGEX search and replace. This legacy app is 20+ years old so the first goal was compatibility. In a follow up article I’ll look at improving security with HMAC.
Note
This post was amended from “the default hashing algorithm changed from MD5 to SHA-256” to “the default hashing algorithm changed from CFMX_COMPAT to SHA-256”. Even though they do the same thing the default was technically CFMX_COMPAT.
Here is a helpful workspace configuration option(s) that allows you to set the default terminal profile (terminal.integrated.defaultProfile.YOUR_OS) to CommandBox in VSCode terminal. This allows you to launch CommandBox by default when you open a terminal in the workspace. Further, you can specify the current working directory (terminal.integrated.cwd) so any new terminal you open in the workspace will start in that directory.
These settings are stored in YOUR_WORKSPACE.code-workspace so you can set them on a per workspace basis.
YOUR_OS = the OS you are running
YOUR_WORKSPACE = whatever the .workspace file is for your current workspace.
This post is a follow up to a topic I originally posted titled Run CommandBox directly inside VSCode Terminal which explained setting up a custom terminal profile (terminal.integrated.profiles.YOUR_OS) for CommandBox.
I’d like to revisit the CFML Formatter (VSCode extension) that I posted on not too long ago and mention the cfformat-ignore functionality.
I had to work on some 15+ year old code today. I won’t say who wrote that code. Cough Me. Cough Embarrassing. Every time I saved a file the CFML Formatter was working overtime to try to figure out how to get the code formatted. Long story short there were a few blocks involving concatenation that ended up throwing errors after they were reformatted. Due to time constraints I needed to put a pin in figuring how to rewrite the offending blocks and just needed cfformat to ignore those blocks.
In a nutshell there are 3 ways to do it depending on the block of code.