Signal Traps Aren't Identical to Kernel's At_Exit Handler
You said in comments that you don't want to use Kernel#at_exit, but Signal#trap is not really an exact synonym. Pragmatically, the behavior of the two methods is demonstrably different. You can see this easily as follows:
# prints nil because variable not in scope; can't raise exceptions
Signal.trap(0) { p defined?(dir); p dir; raise dir }; dir="foo"; exit
# instance variables accessible; won't raise when undefined
Signal.trap(0) { p defined?(@dir); p @dir }; exit
# raises NameError because variable not in scope
at_exit { p defined?(dir); p dir }; dir="foo"; exit
# instance variable in scope, but won't raise when undefined
at_exit { p defined?(@dir); p @dir }; exit
# instance variable in scope; can raise manually
at_exit { p defined?(@dir); p @dir; raise @dir }; exit
If you want to know why they're different, you'll likely have to examine the parser or the underlying implementation of your current Ruby engine. It may be a bug, or a deliberate choice by the Ruby Core Team. Either way, it's clear that they have distinct behavior in Ruby 2.7.1.
Use Instance Variables in At_Exit Handler
As demonstrated, using local variables with a trapped signal presents a scoping problem, and other implementations of your desired code may not be reliable or consistent for your use case. You should register a handler through Kernel#at_exit, and store your temporary directory name in an instance or class variable instead.
Registering a handler through Kernel#at_exit will do what you expect from irb or the command line when @dir is defined as an instance variable in the top-level scope or the handler's registered binding. Pathname is not strictly necessary, but is included to match your current code.
require 'pathname'
require 'tmpdir'
at_exit { @dir.rmtree }
@dir = Pathname.new Dir.mktmpdir
p @dir
Use a Block for Automatic Cleanup
You can avoid scope issues in your signal/exit handlers altogether by using the block form of the Dir#mktmpdir command instead. For example:
require 'tmpdir'
Dir.mktmpdir do |dir|
# do something with dir
end
This will clean up the temporary directory when the block exits, rather than having to register an exit handler or signal trap. In my experience, this is generally much easier to test and debug than deferred closures, but your mileage may certainly vary.